WebRTC 是 Chromium源码third_party的一部分。
webrtc框架图
如果按照通常层次化的思维来组织,从下到上,大概分以下几个层次:
源码结构:
api ;提供了对外的接口,音视频引擎层和 Module 直接的接口。
audio ;音频流的一部分抽象,属于引擎的一部分逻辑。
base ;这一部分还没有学习到,属于 Chromium 项目的一部分,貌似 WebRTC 中用的并不多。
build ;编译脚本。这里需要注意的是,不同平台的代码在下载的时候,获取的工具集是不一样的。
build_overrides ;编译工具。
buildtools ;编译工具链。
call ;主要是媒体流的接口抽象。为媒体引擎和 codec 层提供桥接。这里说的媒体流是 RTP 流。pc 层也抽象了媒体流,那是编码前、或者解码后。
common_audio ;音频算法实现,比如 fft。
common_video ;视频算法实现,比如 h264 协议格式。
data ;测试数据
examples ;WebRTC 使用的例子。提供了 peerconnection_client、peerconnection_server、stun、turn 的 demo。
help ;没有学习到。
infra ;没有学习到。
logging ;WebRTC 的 log 库。
media ;媒体引擎层,包括音频、视频引擎实现。
modules ;WebRTC 把一些逻辑比较独立的抽象为 Module,利于扩展维护。
ortc ;媒体描述协议,类似 sdp 协议。
out ;build 输出目录,这是 webrtc 官方编译指导中示范目录。
p2p ;主要是实现 candidate 收集,NAT 穿越。
pc ;实现 jsep 协议。
resources ;测试数据
rtc_base ;包括 Socket、线程、锁等 OS 基础功能实现。
rtc_tools ;网络监测工具、音视频分析工具。很多工具都是脚本实现。
sdk ;主要是移动端相关实现。
stats ;WebRTC 统计模块实现。
style-guide ;编码规范说明
system_wrappers ;OS 相关功能的封装,比如 cpu、clock 等。
test ;单元测试代码实现,用 gmock
testing ;gmock、gtest等源码,属于整个 Chromium 项目。
third_party ;第三方库依赖。比如,boringssl,abseil-cpp,libvpx等
tools ;公共工具集,整个 Chromium 项目依赖的。
tools_webrtc ;WebRTC 用到的工具集。比如代码检查 valgrind 的使用。
video ;视频 RTP 流的抽象接口,属于视频引擎的一部分。
PeerConnection 的主要实现逻辑就是在 WebRTC 源码的 pc 目录下。
一切都从 PeerConnectionFactory 和 PeerConnection 开始,对外提供 PeerConnectionFactoryInterface 和 PeerConnectionInterface 两个接口类。Factory 类,顾名思义就是创建 PeerConnection 的,下来我们只讨论 PeerConnection。
也许你已经非常熟悉 WebRTC 的 JavaScript 接口。比如,RTCPeerConnection,setLocalDescription、setRemoteDescription、createOffer、createAnswer 等,没错这些。JavaScript 接口的 Native 实现就是在 PeerConnection 中完成的,它也有对应的一套接口。JavaScript 这套接口实现规范是JSEP。 可以说是把这套规范的模型都给实现了。
WebRTC 终端之间的通信协议是 ICE 协议,书包格式采用 SDP 协议。PeerConnection 实现了 SessionDescription 的逻辑。
PeerConnection 抽象了 RtpTransceiver,RtpSender、RtpReceiver 模型,对应了 sdp 中描述的媒体的实现。
WebRTC 将逻辑功能独立、内聚性、复用性强的部分单独抽象为模块。模块在 WebRTC 源码的 modules 目录下,主要是音视频设备、codec、流控等,这里不一一列举了。
Module 抽象了一个接口,源码实现在 modules/include/module.h 中,代码如下:
namespace webrtc{
class Module {
public:
virtual int64_t TimeUntilNextProcess() = 0;
virtual void Process() = 0;
virtual void ProcessThreadAttached(ProcessThread* process_thread) {}
protected:
virtual ~Module() {}
};
} // namespace webrtc
一共三个函数,相对简单,主要是为那些需要定时处理一些任务的模块提供一个统一的抽象。对于集成了 webrtc::Module 类的模块,使用的时候,需要调用 ProcessThread 的 RegisterModule 和 DeRegisterModule 方法向模块执行线程注册和反注册模块。我相信大家都知道,控制、调度逻辑是最复杂易错的,实现起来也是最枯燥乏味,设计不好,很容易重复实现很多代码。所以 WebRTC 索性都封装起来,实现者只需要被动的实现功能逻辑即可。
获取媒体流
第一步:获取视频源videoSource
String frontCameraName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
VideoCapturer videoCapturer = VideoCapturerAndroid.create(frontCameraName);
VideoSource videoSource = factory.createVideoSource(videoCapturer,videoConstraints);
其中videoConstraints是对视频流的一些限制,按如下方法创建。
MediaConstraints videoConstraints = new MediaConstraints();
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxHeight", Integer.toString(pcParams.videoHeight)));
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxWidth", Integer.toString(pcParams.videoWidth)));
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxFrameRate", Integer.toString(pcParams.videoFps)));
videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("minFrameRate", Integer.toString(pcParams.videoFps)));
第二步:获取音频源audioSource
AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
第三步:获得封装VideoTrack/AudioTrack
VideoTrack/AudioTrack 是 VideoSource/AudioSource 的封装,方便他们的播放和传输:
VideoTrack videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource);
AudioTrack audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource);
第四步:获取媒体流localMS
其实 VideoTrack/AudioTrack 已经可以播放了,不过我们先不考虑本地播放。那么如果要把他们发送到对方客户端,我们需要把他们添加到媒体流中:
MediaStream localMS=factory.createLocalMediaStream("ARDAMS");
localMS.addTrack(videoTrack);
localMS.addTrack(audeoTrack);
然后,如果有建立好的连接通道,我们就可以把 localMS 发送出去了。
建立连接通道
WebRTC是基于P2P的,但是在连接通道建立好之前,我们仍然需要服务器帮助传递信令,而且需要服务器帮助进行网络穿透。大体需要如下几个步骤。
第一步:创建PeerConnection的对象。
PeerConnection pc = factory.createPeerConnection(
iceServers,//ICE服务器列表
pcConstraints,//MediaConstraints
context);//上下文,可做监听
PeerConnectionClient:PeerConnection的实现,有了这个类才能进行音视频相关数据通讯;
iceServers 我们下面再说。
pcConstraints是媒体限制,可以添加如下约束:
pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
pcConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
监听器建议同时实现SdpObserver、PeerConnection.Observer两个接口。
第二步:信令交换
建立连接通道时我们需要在WebRTC两个客户端之间进行一些信令交换,我们以A作为发起端,B作为响应端(A call B,假设服务器和A、B已经连接好,并且只提供转发功能,PeerConnection对象为pc ):
A向B发出一个“init”请求(我觉得这步没有也行)。
B收到后“init”请求后,调用pc.createOffer()方法创建一个包含SDP描述符(包含媒体信息,如分辨率、编解码能力等)的offer信令。
offer信令创建成功后会调用SdpObserver监听中的onCreateSuccess()响应函数,在这里B会通过pc.setLocalDescription将offer信令(SDP描述符)赋给自己的PC对象,同时将offer信令发送给A 。
A收到B的offer信令后,利用pc.setRemoteDescription()方法将B的SDP描述赋给A的PC对象。
A在onCreateSuccess()监听响应函数中调用pc.setLocalDescription将answer信令(SDP描述符)赋给自己的PC对象,同时将answer信令发送给B 。
B收到A的answer信令后,利用pc.setRemoteDescription()方法将A的SDP描述赋给B的PC对象。
这样,A、B之间就完成里了信令交换。
第三步:通过ICE框架穿透NAT/防火墙
如果在局域网内,信令交换后就已经可以传递媒体流了,但如果双方不在同一个局域网,就需要进行NAT/防火墙穿透(我是在局域网下测试的,没有穿透,但还是把这方面内容介绍下)。
WebRTC使用ICE框架来保证穿透。ICE全名叫交互式连接建立(Interactive Connectivity Establishment),一种综合性的NAT/FW穿越技术,它是一种框架,可以整合各种NAT/FW穿越技术如STUN、TURN(Traversal Using Relay NAT 中继NAT实现的穿透)。ICE会先使用STUN,尝试建立一个基于UDP的连接,如果失败了,就会去TCP(先尝试HTTP,然后尝试HTTPS),如果依旧失败ICE就会使用一个中继的TURN服务器。使用STUN服务器穿透的结构如下:
我们可以使用Google的stun服务器:stun:stun.l.google.com:19302(Google嘛,你懂得,当然如果有精力可以自己搭建一个stun服务器),那么我们怎么把这个地址告诉WebRTC呢,还记得之前的iceServers吗,就是在创建PeerConnection对象的时候需要的参数,iceServers里面存放的就是进行穿透地址变换的服务器地址,添加方法如下(保险起见可以多添加几个服务器地址,如果有的话):
iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));
然后这个stun服务器地址也需要通过信令交换,同样以A、B客户端为例过程如下:
A、B分别创建PC实例pc(配置了穿透服务器地址) 。
当网络候选可用时,PeerConnection.Observer监听会调用onIceCandidate()响应函数并提供IceCandidate(里面包含穿透所需的信息)的对象。在这里,我们可以让A、B将IceCandidate对象的内容发送给对方。
A、B收到对方发来的candidate信令后,利用pc.addIceCandidate()方法将穿透信息赋给各自的PeerConnection对象。
至此,连接通道完全打通,然后我们只需要将之前获取的媒体流localMS赋给pc即可:
pc.addStream(localMS);//也可以先添加,连接通道打通后一样会触发监听响应。
在连接通道正常的情况下,对方的PeerConnection.Observer监听就会调用onAddStream()响应函数并提供接收到的媒体流。
播放媒体流
WebRTC提供了一种很方便的播放方式:VideoRendererGui,首先设置VideoRendererGui,具体方法如下:
GLSurfaceView videoView = (GLSurfaceView) findViewById(R.id.glview_call);
VideoRendererGui.setView(videoView, runnable);//surface准备好后会调用runnable里的run()函数
然后创建一个VideoRenderer对象,并将其赋给videoTrack:
VideoRenderer renderer = VideoRendererGui.createGui(x, y, width, height);//设置界面
videoTrack.addRenderer(renderer);
WebRTC允许我们实现自己的渲染,我们只需通过VideoRendererGui获取VideoRenderer.Callbacks的对象,渲染后把其作为参数传入到VideoRenderer的构造方法即可。
此外利用VideoRenderer.Callbacks,我们可以动态调整播放界面,如下:
VideoRenderer.Callbacks cbRenderer = VideoRendererGui.create(x, y, width, height, scalingType, mirror);//设置界面
videoTrack.addRenderer(new VideoRenderer(cbRenderer ));
VideoRendererGui.update(cbRenderer ,x, y, width, height, scalingType);//调整界面
信令服务器
信令服务器主要是在客户端打通连接通道前传递信令的,在客户端开启P2P通道后,这个服务器关了也不会影响媒体流传输。
我是用ProjectRTC作为服务器,这个项目里还包括PC客户端的实现,不过我们不用管它们,ProjectRTC项目根目录下的app.js是入口文件,里面设置必要参数,如网口等。我们需要关注的文件是app文件夹下的:socketHandler.js 和 streams.js 文件。
socketHandler.js 是服务器用来和客户端交互的接口,里面的实现网口的监听,每有新的连接接入,都在这里进行存储。通过分析这个文件可以发现,所有连接的socket都存放在sockets对象中,标志是socket.id,socket的收发函数也是在这里定。
streams.js是一个存储的工具类,里面有两个成员:id和name,这个文件用来存放已经准备好打通连接通道的客户端的信息,name是客户端的名字,id是连接对应客户端的socket的id 。
如果我们要实现客户端的信令交互,只需要修改这两个文件即可(实际上基本不用改)。
主要的API 有VideoCapturerAndroid, VideoRenderer, MediaStream, PeerConnection 和 PeerConnectionFactory