什么是 WebRTC ?
WebRTC是“网络实时通信”(Web Real Time Communication)的缩写。它最初是为了解决浏览器上视频通话而提出的,即两个浏览器之间直接进行视频和音频的通信,不经过服务器。后来发展到除了音频和视频,还可以传输文字和其他数据,是一种免费开源的实时通信技术,集成了音视频采集、编解码、流媒体传输、渲染等功能,并在Native代码基础上,封装了简单的JavaScript api,仅通过几行代码即可实现点对点通信,且具有良好的跨平台特性,目前主流的浏览器都已支持。
WebRTC主要让浏览器具备三个作用。
- 获取音频和视频流 MediaStream
- 进行音频和视频通信 RTCPeerConnection
- 进行任意数据的通信 RTCDataChannel
MediaStream浏览器兼容问题可通过引用 webrtc.github.io/adapter/ada… 解决
WebRTC 的通信至少需要两种服务配合:
信令阶段需要双向通信服务辅助信息交换。 STUN,TURN辅助实现 NAT 穿越。
基本概念
- SDP: 即会话描述协议,主要保存当前会话的媒体和传输信息,其中媒体信息包括媒体类型、传输协议、媒体格式等,传输信息包括媒体的远程地址信息、带宽等;它由多行kv格式的文本信息组成,具体可参考这里。(tools.ietf.org/pdf/draft-n…
- Offer: 通信的发起方对自己的sdp描述
- Answer: 通信的接收方对自己的sdp描述 信令: 协调发起方、接收方通信的数据信息,其中包括sdp描述信息、会话控制信息(节点加入、退出及各类的业务控制信息等)、网络信息、错误信息等。
webrtc通信
基于webrtc的点对点音视频通信示意图如下图所示:
其具体的流程如下:
- 客户端A初始化本地音视频设备,创建一个用于offer的SDP对象,其中SDP对象中保存当前音视频的相关信息;
- 客户端A通过信令服务器将SDP信息发送给客户端B;
- 客户端B在接收到客户端A发送的SDP信息并保存后,初始化本地音视频设备并创建用于answer的SDP对象;
- 客户端B通过信令服务器将SDP信息发送给客户端A;
- 客户端A、B通过交换SDP等信息,建立P2P通道进行音视频传输;
Webrtc ICE(交互式连接建立)
在现实世界中,客户端A、B大部分位于NAT之后,只具有一个内网访问的私有ip,不足以提供足够的信息来建立一条端对端的连接。为了克服由NAT产生的网络问题,一般情况下,webrtc采用ICE框架,通过ICE来找到一条对等连接的最佳道路。 举个栗子,小A和小B是好朋友,某天他们都换了手机号,而且都绑定了一个手机短号(短号不在一个集团网中),然而都忘记新的号码是多少。为了两个人通话联系,小A尝试用短号拨号不通后,小A拨打114询问自己的电话号码,114看到来电显示后将手机号告诉小A;小B以同样的方式获得了自己的手机号;这样两人就可以相互通话了。 类似于上边的例子,客户端A和客户端B处在不同的内网环境中,首先ICE尝试采用从操作系统和网卡获得的主机地址建立连接,如果连接建立失败,ICE会发送binding request给STUN服务器,服务器探测到客户端A的公网地址后将信息加在binding request中,并返回给客户端A,这样客户端A获取到了自己的公网地址。以同样的方式客户端B获取到自己的公网地址。这样,客户端A、B就可以将SDP中的地址信息替换为公网地址进行通信了。
Webrtc (多对对) Webrtc (1对多)
getUserMedia
navigator.mediaDevices或者navigator.mediaDevices.getUserMedia方法目前主要用于在浏览器中获取音频(通过麦克风)和视频(通过摄像头),将来可以用于获取任意数据流,比如光盘和传感器。
let constraints={
video: {
// 设定理想值、最大值、最小值
width: { min: 960, ideal: 1280, max: 1920 },
height: { min: 480, ideal: 720, max: 1080 },
//设置采集帧率
frameRate: {min:30,max:120},
//前置
facingMode: "user",
//后置
//facingMode: { exact: "environment" } }
// 也可以指定设备 id,
// 通过 navigator.mediaDevices.enumerateDevices() 可以获取到支持的设备
//{ video: { deviceId: myCameraDeviceId } }
//还有一个比较有意思的就是设置视频源为屏幕,但是目前只有火狐支持了这个属性。
mediaSource: 'screen'
},
audio:false,
};
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess()).catch(onError());
function onSuccess(stream) {
let video = document.getElementById("localVideo");
video.srcObject=stream;
}
function onError(error) {
console.log("navigator.mediaDevices.getUserMedia error: ", error);
}
<body>
<video id="localVideo" autoplay playsinline></video>
</body>
第一个参数(constraints)是个json对象,表示需要获取的媒体设备,以及媒体信息的相关参数设置 详细可见 MediaTrackSettings
interface MediaTrackSettings {
//屏幕高宽比
aspectRatio?: number;
//自动增益控制
autoGainControl?: boolean;
// 声道数,是指支持能不同发声(注意是不同声音)的音响的个数。 单声道:1个声道 双声道:2个声道 立体声道:默认为2个声道 立体声道(4声道):4个声道
channelCount?: number;
//设备id
deviceId?: string;
//回波消除
echoCancellation?: boolean;
//摄像头类型 前置or后置
facingMode?: string;
//帧率 即单位时间内帧的数量,单位为:帧/秒 或fps(frames per second)。如动画书中, 一秒内包含多少张图片,图片越多,画面越顺滑,过渡越自然。 帧率的一般以下几个典型值:
24/25 fps:1秒 24/25 帧,一般的电影帧率。
30/60 fps:1秒 30/60 帧,游戏的帧率,30帧可以接受,60帧会感觉更加流畅逼真。
85 fps以上人眼基本无法察觉出来了,所以更高的帧率在视频里没有太大意义。
frameRate?: number;
//设备组id
groupId?: string;
//视频高度
height?: number;
//延迟
latency?: number;
//噪声抑制
noiseSuppression?: boolean;
//尺寸调整模式
resizeMode?: string;
//采样率 采样率即采样的频率。采样率要大于原声波频率的2倍,人耳能听到的最高频率为20kHz,所以为了满足人耳的听觉要求,采样率至少为40kHz,通常为44.1kHz,更高的通常为48kHz。
sampleRate?: number;
//样本量 采样位数涉及到声波的振幅量化。波形振幅在模拟信号上也是连续的样本值,而在数字信号中,信号一般是不连续的,所以模拟信号量化以后,只能取一个近似的整数值,为了记录这些振幅值,采样器会采用一个固定的位数来记录这些振幅值,通常有8位、16位、32位。位数越多,记录的值越准确,还原度越高。 由于数字信号是由0,1组成的,因此,需要将幅度值转换为一系列0和1进行存储,也就是编码,最后得到的数据就是数字信号:一串0和1组成的数据。
sampleSize?: number;
//视频宽度
width?: number;
}
onSuccess是一个回调函数,在获取多媒体设备成功时调用; onError也是一个回调函数,在取多媒体设备失败时调用。 如果网页使用了getUserMedia方法,浏览器就会询问用户,是否同意浏览器调用麦克风或摄像头。如果用户同意,就调用回调函数onSuccess;如果用户拒绝,就调用回调函数onError。 onSuccess回调函数的参数是一个数据流对象stream。stream.getAudioTracks方法和stream.getVideoTracks方法,分别返回一个数组,其成员是数据流包含的音轨和视轨(track)。使用的声音源和摄影头的数量,决定音轨和视轨的数量。比如,如果只使用一个摄像头获取视频,且不获取音频,那么视轨的数量为1,音轨的数量为0。每个音轨和视轨,有一个kind属性,表示种类(video或者audio),和一个label属性(比如FaceTime HD Camera (Built-in))。 onError回调函数接受一个Error对象作为参数。Error对象的code属性有如下取值,说明错误的类型。 PERMISSION_DENIED:用户拒绝提供信息。 NOT_SUPPORTED_ERROR:浏览器不支持硬件设备。 MANDATORY_UNSATISFIED_ERROR:无法发现指定的硬件设备。 截屏 Canvas API有一个ctx.drawImage(video, 0, 0)方法,可以将视频的一个帧转为canvas元素。这使得截屏变得非常容易。 捕获的限定条件 比如限定只能录制高清(或者VGA标准)的视频。
var hdConstraints = {
video: {
mandatory: {
minWidth: 1280,
minHeight: 720
}
}
};
var vgaConstraints = {
video: {
mandatory: {
maxWidth: 640,
maxHeight: 360
}
}
};
MediaStreamTrack.getSources() 如果本机有多个摄像头/麦克风,这时就需要使用MediaStreamTrack.getSources方法指定,到底使用哪一个摄像头/麦克风。
MediaStreamTrack.getSources(function(sourceInfos) {
var audioSource = null;
var videoSource = null;
for (var i = 0; i != sourceInfos.length; ++i) {
var sourceInfo = sourceInfos[i];
if (sourceInfo.kind === 'audio') {
console.log(sourceInfo.id, sourceInfo.label || 'microphone');
audioSource = sourceInfo.id;
} else if (sourceInfo.kind === 'video') {
console.log(sourceInfo.id, sourceInfo.label || 'camera');
videoSource = sourceInfo.id;
} else {
console.log('Some other kind of source: ', sourceInfo);
}
}
sourceSelected(audioSource, videoSource);
});
function sourceSelected(audioSource, videoSource) {
var constraints = {
audio: {
optional: [{sourceId: audioSource}]
},
video: {
optional: [{sourceId: videoSource}]
}
};
navigator.mediaDevices.getUserMedia(constraints, onSuccess, onError);
}
MediaStreamTrack.getSources方法的回调函数,可以得到一个本机的摄像头和麦克风的列表,然后指定使用最后一个摄像头和麦克风。 RTCPeerConnectionl RTCPeerConnection的作用是在浏览器之间建立数据的“点对点”p2p(peer to peer)通信,也就是将浏览器获取的麦克风或摄像头数据,传播给另一个浏览器。这里面包含了很多复杂的工作,比如信号处理、多媒体编码/解码、点对点通信、数据安全、带宽管理等等。 不同客户端之间的音频/视频传递,是不用通过服务器的。但是,两个客户端之间建立联系,需要通过服务器。服务器主要转递两种数据。 通信内容的元数据:打开/关闭对话(session)的命令、媒体文件的元数据(编码格式、媒体类型和带宽)等。 网络通信的元数据:IP地址、NAT网络地址翻译和防火墙等。 WebRTC协议没有规定与服务器的通信方式,因此可以采用各种方式,比如WebSocket。通过服务器,两个客户端按照Session Description Protocol(SDP协议)交换双方的元数据。
var signalingChannel = createSignalingChannel();
var pc;
var configuration ={};
// run start(true) to initiate a call
function start(isCaller) {
pc = new RTCPeerConnection(configuration);
// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};
// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};
// get the local stream, show it in the local video element and send it
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
if (isCaller)
pc.createOffer(gotDescription);
else
pc.createAnswer(pc.remoteDescription, gotDescription);
function gotDescription(desc) {
pc.setLocalDescription(desc);
signalingChannel.send(JSON.stringify({ "sdp": desc }));
}
});
}
signalingChannel.onmessage = function (evt) {
if (!pc)
start(false);
var signal = JSON.parse(evt.data);
if (signal.sdp)
pc.setRemoteDescription(new RTCSessionDescription(signal.sdp));
else
pc.addIceCandidate(new RTCIceCandidate(signal.candidate));
};
RTCDataChannel
RTCDataChannel的作用是在点对点之间,传播任意数据。它的API与WebSockets的API相同。
var pc = new RTCPeerConnection(servers,
{optional: [{RtpDataChannels: true}]});
pc.ondatachannel = function(event) {
receiveChannel = event.channel;
receiveChannel.onmessage = function(event){
document.querySelector("div#receive").innerHTML = event.data;
};
};
sendChannel = pc.createDataChannel("sendDataChannel", {reliable: false});
document.querySelector("button#send").onclick = function (){
var data = document.querySelector("textarea#send").value;
sendChannel.send(data);
};
外部函数库 由于这两个API比较复杂,一般采用外部函数库进行操作。目前,视频聊天的函数库有SimpleWebRTC、easyRTC、webRTC.io,点对点通信的函数库有PeerJS、Sharefest。
IceServers
我们使用coturn
docker
docker run -d -p 3478:3478 -p 49152-65535:49152-65535/udp --name coturn instrumentisto/coturn
coturn docker资源地址:
centos7
- 安装相关依赖
yum install -y make gcc cc gcc-c++ wget openssl-devel libevent libevent-devel
- 下载可以编译的源码包
wget https://coturn.net/turnserver/v4.5.1.2/turnserver-4.5.1.2.tar.gz
- 解压
tar -zxvf turnserver-4.5.1.2.tar.gz
- 编译安装
./configure --prefix=/home/webrtc/coturn```
make && make install
- 设置环境变量
vim ~/.bashrc
export turnserver_home=/home/webrtc/coturn export PATH=$PATH:$turnserver_home/bin ```
source ~/.bashrc
- 编辑配置文件
find /usr -name turnserver.conf
vim /home/webrtc/coturn/share/examples/turnserver/etc/turnserver.conf
监听的网卡
listening-device=eth0
监听的端口
listening-port=3478
监听的内网id
listening-ip=172.17.19.101
#监听的外网ip
external-ip=122.51.255.169
# 设置账号密码
user=snjx:suonanjiexi
#cli密码
cli-password=suonanjiexi
指定配置文件启动服务
turnserver -v -r 外网ip -a -o -c /usr/local/turnserver/share/examples/turnserver/etc/turnserver.conf
turnserver -v -r 122.51.255.169 -a -o -c /home/webrtc/coturn/share/examples/turnserver/etc/turnserver.conf```
* 测试 ICEServer
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
turn:122.51.255.169:3478
stun:122.51.255.169:3478
const iceConfiguration = {
"iceServers": [
{
url: 'stun:122.51.255.169:3478'
},
{ // 谷歌的公共服务
url: "stun:stun.l.google.com:19302"
},
{
url: 'turn:122.51.255.169:3478',
username: snjx, // 用户名
credential: suonanjiexi // 密码
}
]
}
let localPc = new RTCPeerConnection(iceServers);
let remotePc = new RTCPeerConnection(iceServers);
参数配置了两个 url,分别是 STUN 和 TURN,这便是 WebRTC 实现点对点通信的关键,也是一般 P2P 连接都需要解决的问题:NAT穿越。