这一篇咱们来说一下WebRTC协议,以前我总结过一篇各类网络协议的总结,没看过的朋友建议先看下这篇web知识梳理,有助于加深这篇关于WebRTC的理解。android
WebRTC是由Google主导的,由一组标准、协议和JavaScript API组成,用于实现浏览器之间(端到端之间)的音频、视频及数据共享。WebRTC不须要安装任何插件,经过简单的JavaScript API就可使得实时通讯变成一种标准功能。web
如今各大浏览器以及终端已经逐渐加大对WebRTC技术的支持。下图是webrtc官网给出的如今已经提供支持了的浏览器和平台。 json
在深刻讲解协议以前,咱们先来看实例。咱们先来看下在Android中实现一个WebRTC的代码示例。api
首先,引入WebRTC依赖包,这里我是使用Nodejs下的socket.io库实现WebRTC信令服务器的,因此也要引入socket.io依赖包。浏览器
dependencies {
implementation 'io.socket:socket.io-client:1.0.0'
implementation 'org.webrtc:google-webrtc:1.0.+'
implementation 'pub.devrel:easypermissions:1.0.0'
}
复制代码
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(getApplicationContext())
.setEnableVideoHwAcceleration(true)
.createInitializationOptions());
//建立PeerConnectionFactory
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
mPeerConnectionFactory = new PeerConnectionFactory(options);
//设置视频Hw加速,不然视频播放闪屏
mPeerConnectionFactory.setVideoHwAccelerationOptions(mEglBase.getEglBaseContext(), mEglBase.getEglBaseContext());
复制代码
private void initConstraints() {
iceServers = new LinkedList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:23.21.150.121").createIceServer());
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
pcConstraints = new MediaConstraints();
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
sdpConstraints = new MediaConstraints();
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
}
复制代码
布局文件写两个控件,一个显示本地视频流,一个显示远端视频流。缓存
<org.webrtc.SurfaceViewRenderer
android:id="@+id/view_local"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center_horizontal"/>
<org.webrtc.SurfaceViewRenderer
android:id="@+id/view_remote"
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="50dp"/>
复制代码
并对这两个控件进行一些基础设置tomcat
//初始化localView
localView.init(mEglBase.getEglBaseContext(), null);
localView.setKeepScreenOn(true);
localView.setMirror(true);
localView.setZOrderMediaOverlay(true);
localView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
localView.setEnableHardwareScaler(false);
//初始化remoteView
remoteView.init(mEglBase.getEglBaseContext(), null);
remoteView.setMirror(false);
remoteView.setZOrderMediaOverlay(true);
remoteView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
remoteView.setEnableHardwareScaler(false);
复制代码
mVideoCapturer = createVideoCapture(this);
VideoSource videoSource = mPeerConnectionFactory.createVideoSource(mVideoCapturer);
mVideoTrack = mPeerConnectionFactory.createVideoTrack("videtrack", videoSource);
//设置视频画质 i:width i1 :height i2:fps
mVideoCapturer.startCapture(720, 1280, 30);
AudioSource audioSource = mPeerConnectionFactory.createAudioSource(new MediaConstraints());
mAudioTrack = mPeerConnectionFactory.createAudioTrack("audiotrack", audioSource);
//播放本地视频
mVideoTrack.addRenderer(new VideoRenderer(localView));
//建立媒体流并加入本地音视频
mMediaStream = mPeerConnectionFactory.createLocalMediaStream("localstream");
mMediaStream.addTrack(mVideoTrack);
mMediaStream.addTrack(mAudioTrack);
复制代码
要想从远端获取数据,咱们就必须建立 PeerConnection 对象。该对象的用处就是与远端创建联接,并最终为双方通信提供网络通道。安全
PeerConnection peerConnection = factory.createPeerConnection(
iceServers, //ICE服务器列表,干什么用下面会详细解释
constraints, //MediaConstraints
this); //Context
复制代码
注意这里要把地址换成你的服务端的地址,我这里WebRTC信令服务端使用的NodeJS编写,而后用的是本身本地的tomcat地址。bash
//链接服务器
try {
mSocket = IO.socket("http://192.168.31.172:3000/");
} catch (URISyntaxException e) {
e.printStackTrace();
}
mSocket.on("SomeOneOnline", new Emitter.Listener() {
@Override
public void call(Object... args) {
isOffer = true;
if (mPeer == null) {
mPeer = new Peer();
}
mPeer.peerConnection.createOffer(mPeer, sdpConstraints);
}
}).on("IceInfo", new Emitter.Listener() {
@Override
public void call(Object... args) {
try {
JSONObject jsonObject = new JSONObject(args[0].toString());
IceCandidate candidate = null;
candidate = new IceCandidate(
jsonObject.getString("id"),
jsonObject.getInt("label"),
jsonObject.getString("candidate")
);
mPeer.peerConnection.addIceCandidate(candidate);
} catch (JSONException e) {
e.printStackTrace();
}
}
}).on("SdpInfo", new Emitter.Listener() {
@Override
public void call(Object... args) {
if (mPeer == null) {
mPeer = new Peer();
}
try {
JSONObject jsonObject = new JSONObject(args[0].toString());
SessionDescription description = new SessionDescription
(SessionDescription.Type.fromCanonicalForm(jsonObject.getString("type")),
jsonObject.getString("description"));
mPeer.peerConnection.setRemoteDescription(mPeer, description);
if (!isOffer) {
mPeer.peerConnection.createAnswer(mPeer, sdpConstraints);
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
mSocket.connect();
复制代码
@Override
public void onAddStream(MediaStream mediaStream) {
remoteVideoTrack = mediaStream.videoTracks.get(0);
remoteVideoTrack.addRenderer(new VideoRenderer(remoteView));
}
复制代码
/**
*DataChannel.Init 可配参数说明:
*ordered:是否保证顺序传输;
*maxRetransmitTimeMs:重传容许的最长时间;
*maxRetransmits:重传容许的最大次数;
**/
DataChannel.Init init = new DataChannel.Init();
dataChannel = peerConnection.createDataChannel("dataChannel", init);
复制代码
发送消息:服务器
byte[] msg = message.getBytes();
DataChannel.Buffer buffer = new DataChannel.Buffer(
ByteBuffer.wrap(msg),
false);
dataChannel.send(buffer);
复制代码
onMessage()回调收消息:
ByteBuffer data = buffer.data;
byte[] bytes = new byte[data.capacity()];
data.get(bytes);
String msg = new String(bytes);
复制代码
下面这张图清楚的描述了WebRTC的协议分层,该图引自《web性能权威指南》,若有侵权,立马删掉。
WebRTC实时通讯传输音视频的场景,讲究的是实时,当下,处理音频和视频流的应用必定要补偿间歇性的丢包,因此实时性的需求是大于可靠性的需求的。
若是使用TCP当传输层协议的话,若是中间出现丢包的状况,那么后续的全部的包都会被缓冲起来,由于TCP讲究可靠、有序,若是不清楚的朋友能够去看我上一篇关于TCP的内容讲解,web知识梳理。而UDP则正好相反,它只负责有什么消息我就传过去,不负责安全,不负责有没有到达,不负责交付顺序,这里从底层来看是知足WebRTC的需求的,因此WebRTC是采用UDP来当它的传输层协议的。
固然这里UDP只是做为传输层的基础,想要真正的达到WebRTC的要求,咱们就要来分析在传输层之上,WebRTC作了哪些操做,用了哪些协议,来达到WebRTC的要求了。
RTCPeerConnection表明一个由本地计算机到远端的WebRTC链接。 该接口提供了建立,保持,监控,关闭链接的方法的实现,简而言之就是表明了端到端之间的一条通道。api调用上面代码里已经看到了,接下来咱们就一点点的来剖析这条通道都用了哪些协议。
上面咱们也提到了UDP其实只是在IP层的基础上作了一些简单封装而已。而WebRTC若是要实现端到端的通讯效果的话,一定要面临端到端之间不少层防火墙,NAT设备阻隔这些一系列的问题。以前我试过写了原生的webrtc 发现只要不在同一段局域网下面,常常会出现掉线连不上的状况。相同的道理这里就须要作 NAT 穿透处理了。
NAT穿透是啥,在讲NAT穿透以前咱们须要先提几个概念:
就是这个NAT(NetWork Address Translation),它容许单个设备(好比路由器)充当Internet(公有IP)和专有网络(私有IP)之间的代理。因此咱们就能够经过这个NAT来处理不少层防火墙后那个设备是私有IP的问题。
路通了,那么就有另外一个问题了,两个WebRTC客户端之间,大几率会存在A不知道B的能够直接发送到的IP地址和端口,B也不知道A的,那么又该如何通讯呢?
这就要说到ICE了,也就是交互式链接创建。ICE容许WebRTC克服显示网络复杂性的框架,找到链接同伴的最佳途径并链接起来。
在大多数的状况下,ICE将会使用STUN服务器,其实使用的是在STUN服务器上运行的STUN协议,它容许客户端发现他们的公共IP地址以及他们所支持的NAT类型,因此理所固然STUN服务器必须架设在公网上。在大多数状况下,STUN服务器仅在链接设置期间使用,而且一旦创建该会话,媒体将直接在客户端之间流动。
具体过程让咱们来看图会更清楚,WebRTC两个端各自有一个STUN服务器,经过STUN服务器来设置链接,一旦创建链接会话,媒体数据就能够直接在两个端之间流动。
刚才也说了大多数的状况,若是发生STUN服务器没法创建链接的状况的话,ICE将会使用TURN中继服务器,TURN是STUN的扩展,它容许媒体遍历NAT,而不会执行STUN流量所需的“一致打孔”,TURN服务器实际上在WebRTC对等体之间中继媒体,因此我这里理解的话使用TURN就很难被称为端对端之间通讯了。 一样,咱们画个图来形容TURN中继服务器的数据流动方式:
WebRTC 以彻底托管的形式提供媒体获取和交付服务:从摄像头到网络,再从网络到屏幕。 从上面的Android demo中咱们能够看到,咱们除了一开始制定媒体流的约束之外,编码优化、处理丢包、网络抖动、错误恢复、流量、控制等等操做咱们都没作,都是WebRTC本身来控制的。这里WebRTC 是怎么优化和调整媒体流的品质的呢? 其实WebRTC 只是重用了 VoIP 电话使用的传输 协议、通讯网关和各类商业或开源的通讯服务:
安全实时传输协议(SRTP,Secure Real-time Transport Protocol) 经过 IP 网络交付音频和视频等实时数据的标准安全格式。
安全实时控制传输协议(SRTCP,Secure Real-time Control Transport Protocol) 经过 SRTP 流交付发送和接收方统计及控制信息的安全控制协议。
咱们都知道UDP是不安全的,可是WebRTC要求全部传输的数据(音频、视频和自定义应用数据)都必须加密,因此这里就要引入一个DTLS协议的概念。
DTLS说白了,其实就是由于TLS没法保证UDP上传输的数据的安全,因此在现存的TLS协议架构上提出了扩展,用来支持UDP。其实就是TLS的一个支持数据报传输的版本。
既然知道了DTLS能够说是TLS的扩展版之后,咱们再来看看dtls解决了哪些问题。首先先来看TLS的问题,刚才咱们也提到了TLS不能直接用于数据报环境,主要的缘由是包可能会出现丢失或者重排序的状况,而TLS没法处理这种不可靠性,而没法处理这种不可靠性就带来了两个问题:
那么DTLS是如何在尽量与TLS相同的状况下解决以上两个问题的呢?
首先在DTLS中,每一个握手消息都会在握手的时候分配一个序列号和分段偏移字段,当收消息的那一方收到一个握手消息的时候,会根据这个序列号来判断是不是指望的下一个消息,若是不是则放入队列中,这样就知足了有序交付的条件,若是顺序不对就报错,跟TLS同样。而分段偏移字段是为了补偿UDP报文的1500字节大小限制问题。
至于丢包问题,DTLS采用了两端都使用一个简单的重传计时器的方法,仍是上面序列号为1到10的例子,若是A发给B一个序列号为5的消息,而后但愿从B那里获取到序列号为6的消息,可是没收到,超时了,A就知道他发的5或者B给的6这个消息丢失了,而后就会从新发送一个重传包,也就是5这个消息。
为保证过程完整,A和B两端都要生成自已签名的证书,而WebRTC会自动为每一端生成自已签名的证书,而后按照常规的 TLS 握手协议走。
除了传输音频和视频数据,WebRTC 还支持经过 DataChannel API 在端到端之间传 输任意应用数据。DataChannel 依赖于 SCTP(Stream Control Transmission Protocol,流控制传输协议),而 SCTP 在两端之间创建的 DTLS 信道之上运行的。
DataChannel api调用和WebSocket相似,上面的Android项目中咱们已经讲过了,接下来咱们来详细讲下DataChannel所依赖的SCTP协议。
SCTP同时具有了TCP和UDP中最好的功能:面向消息的 API、可配置的可靠性及交付语义,并且内置流量和拥塞控制机制。
由于自己UDP相对TCP来讲比较简单,以前也提到UDP只是对IP层的一个简单封装而已,因此这里咱们就经过比较TCP和SCTP的区别来简单的讲讲SCTP究竟是什么东西和为何具有TCP和UDP二者最好的功能。
TCP是单流有序传输,SCTP是多流可配置传输 上一篇web知识梳理中咱们也讲过,TCP在一条链接中能够复用TCP链接,是单流的,并且是有顺序的,若是一条消息出了问题的话,后面的全部消息都会出现阻塞的状况,强调顺序。而SCTP能够区分多条不一样的流,不一样的流之间传输数据互不干扰,在有序和无序的问题上,SCTP是可配置的,又能够像TCP那样交付次序可序化,也能够像UDP那样乱序交付。
TCP是单路径传输,SCTP是多路径传输 SCTP两端之间的链接能够绑定多条IP,只要有一条链接是通的,那么就是通的,熟悉TCP的朋友应该都知道,TCP之间只能用一个IP来链接。
TCP链接创建是三次握手,SCTP则须要四次握手 上一篇web知识梳理中咱们也已经讲过TCP的三次握手了,SCTP的四次握手比TCP多了一个步骤:server端在收到链接请求时,不会像TCP三次握手那样子收到请求消息之后立马分配内存,将其缓存起来,而是返回一个COOKIE消息。 client端须要回送这个COOKIE,server端对这个COOKIE进行校验之后,从cookie中从新获取有效信息(好比对端地址列表),两端之间才会链接成功。
TCP以字节为单位传输,SCTP以数据块为单位传输 块是SCTP 分组中的最小通讯单位,核心概念与HTTP 2.0分帧层中的那些概念基本同样,没看过的朋友能够参考web知识梳理
ok,到这里基本把WebRTC通讯协议的应用,以及要用到的协议啊概念啊什么的都过了一遍,要实现低延迟的,端到端的通讯传输不是一件容易的事情。相信随着WebRTC不断的完善,支持的端也会愈来愈多,性能也会愈来愈完善。