WebRTC 是一个免费开源的项目,在实时音视频通讯方面具有广泛的应用。它通过简单的 API 为浏览器和移动端设备提供实时通信(RTC)能力。为了最好地服务于这个目的,WebRTC 组件还在被不断的优化中。官方团队的目的就是通过一组公共的协议能够帮助浏览器、移动端和物联网设备实现功能丰富且高质量的通讯。WebRTC 在进行实时音视频通讯过程中需要依赖特定的多媒体数据传输通道,我们今天就来了解一下这个传输通道的建立过程。
熟悉 WebRTC 的小伙伴一定知道 PeerConnection 这个概念,是的,WebRTC 实现多媒体数据的传输就是依赖 PeerConnection 通道。下面我们就来详细介绍一下。
一、全局初始化
在正式创建 PeerConnection 之前,需要进行一些全局模块的初始化,设置性能开关,比如开启视频编码纠错机制 FlexFEC、启动因特尔 VP8 硬件加速、关闭 WebRTC 的自动增益控制,启动日志打印等。下面以移动端的安卓设备和 WebRTC 76 版本为例进行介绍,参考代码如下:
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(appContext)
.setFieldTrials(fieldTrials)
.setEnableInternalTracer(true)
.createInitializationOptions());
完成全局模块的初始化后,就可以进行 PeerConnection 的创建了。
二、PeerConnectionFactory
细心的话,你就会发现上文进行全局初始化处理的时候,使用的就是类的方法。同时,通过名字我们就可以知道 PeerConnectionFactory 是一个工厂类,PeerConnectionFactory 工厂类的实例在后续创建视频编码器和解码器的时候扮演着重要角色。
创建 PeerConnectionFactory 工厂类实例时,完成了很多 PeerConnection 通道、音频和视频的设置工作。下面分别介绍一下,这对于我们理解 PeerConnectionFactory 工厂类的功能有非常大的帮助作用。
1. PeerConnection 通道
全局的 PeerConnection 参数决定了是否打印 PeerConnection 相关的底层日志,参考代码如下:
if (peerConnectionParameters.tracing) {
PeerConnectionFactory.startInternalTracingCapture(
Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "webrtc-trace.txt");
}
2. 音频设置
如果我们不主动设置 AAC 或者 Opus 的音频编码,那么 WebRTC 会默认设置音频编码为 ISAC。ISAC 的全拼是 Internet Speech Audio Codec,由 GIPS 公司开发,是一个免费且开源的音频编码格式,非常适合 VOIP 的应用场景。
WebRTC 还提供了保存音频原始数据的接口,可以用来定位一些音频采集的问题,如果我们发送出去的音频存在噪音或者失真等问题,我们可以优先考虑是不是采集的原始音频数据就存在问题,如果原始音频数据没有问题,再考虑是不是编码、传输、解码、播放等模块导致的,毕竟网络丢包是实际使用过程中最常见的原因之一。而这个接口就可以用来帮助我们定位采集的音频数据是否正确。
尽管,可以通过设置项实现保存音频原始数据到指定的文件中,但是如果底层已经启动 OpenSL ES 的话,那么该设置项就不会生效了。同时,还设置了音频采集和播放的相关模块,作用到安卓系统的硬件设备麦克风和扬声器上。参考代码如下:
preferIsac = peerConnectionParameters.audioCodec != null && peerConnectionParameters.audioCodec.equals(AUDIO_CODEC_ISAC);
if (peerConnectionParameters.saveInputAudioToFile) {
if (!peerConnectionParameters.useOpenSLES) {
Log.d(TAG, "Enable recording of microphone input audio to file");
saveRecordedAudioToFile = new RecordedAudioToFileController(executor);
}
else {
Log.e(TAG, "Recording of input audio is not supported for OpenSL ES");
}
}
final AudioDeviceModule adm = createJavaAudioDevice();
3. 视频设置
设置视频编码类型,一般修改后的 WebRTC 都会支持 H264、VP8、VP9,默认是不支持 H264 的,就像不支持音频编码格式 AAC 一样。另外,还会设置软硬编码和软硬解码,一般软编码和软解码是对应的,硬编码和硬解码是对应的。参考代码如下:
final boolean enableH264HighProfile = VIDEO_CODEC_H264_HIGH.equals(peerConnectionParameters.videoCodec);
final VideoEncoderFactory encoderFactory;
final VideoDecoderFactory decoderFactory;
if (peerConnectionParameters.videoCodecHwAcceleration) {
encoderFactory = new DefaultVideoEncoderFactory(
rootEglBase.getEglBaseContext(), true /* enableIntelVp8Encoder */, enableH264HighProfile);
decoderFactory = new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext());
}
else {
encoderFactory = new SoftwareVideoEncoderFactory();
decoderFactory = new SoftwareVideoDecoderFactory();
}
factory = PeerConnectionFactory.builder()
.setOptions(options)
.setAudioDeviceModule(adm)
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
.createPeerConnectionFactory();
Log.d(TAG, "Peer connection factory created.");
adm.release();
三、PeerConnection
PeerConnection 可以理解为 WebRTC 的多媒体数据传输通道,在整个实时音视频通讯过程中扮演着重要角色。同时,PeerConnection 又是 WebRTC 的三大对外封装接口之一。
PeerConnection 实例的创建依赖上文讲到 PeerConnectionFactory 实例,下面就来详细看一下。RTCConfiguration 类是 PeerConnection 相关的配置参数类,包含了 ICE 服务器、ICE-TCP、bundle 策略、RTCP 多路复用策略、ECDSA 加密、DTLS 加密,SDP 语义等内容。
PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(signalingParameters.iceServers);
rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE;
rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE;
rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
rtcConfig.keyType = PeerConnection.KeyType.ECDSA;
rtcConfig.enableDtlsSrtp = !peerConnectionParameters.loopback;
rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
peerConnection = factory.createPeerConnection(rtcConfig, pcObserver);
另外,还会根据需要动态的启用 DataChannel,DataChannel 通道是一个非常重要的数据通道,有些厂商已经把它作为信令传输通道,参考代码如下:
if (dataChannelEnabled) {
DataChannel.Init init = new DataChannel.Init();
init.ordered = peerConnectionParameters.dataChannelParameters.ordered;
init.negotiated = peerConnectionParameters.dataChannelParameters.negotiated;
init.maxRetransmits = peerConnectionParameters.dataChannelParameters.maxRetransmits;
init.maxRetransmitTimeMs = peerConnectionParameters.dataChannelParameters.maxRetransmitTimeMs;
init.id = peerConnectionParameters.dataChannelParameters.id;
init.protocol = peerConnectionParameters.dataChannelParameters.protocol;
dataChannel = peerConnection.createDataChannel("ApprtcDemo data", init);
}
四、创建音视频流
1. 创建音频流
PeerConnection 创建完成后,会紧接着创建音频 track 和音频 source,音频 track 的创建依赖于音频 source 的创建,但是 PeerConnection 是直接作用在音频 track 上的,因此会调用 addTrack() 方法绑定对应的音频 track 到 PeerConnection 对象上去。参考代码如下:
audioSource = factory.createAudioSource(audioConstraints);
localAudioTrack = factory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
localAudioTrack.setEnabled(enableAudio);
2. 创建视频流
PeerConnection 创建完成后,会紧接着创建视频 track 和视频 source,视频 track 的创建依赖于视频 source 的创建,但是 PeerConnection 是直接作用在视频 track 上的,因此会调用 addTrack() 方法绑定对应的视频 track 到 PeerConnection 对象上去。
但是,创建视频 track 和 视频 source 又与音频有所不同,因为视频需要满足本地预览的需要,VideoCapturer 对象实例初始化需要绑定视频 source 的监听事件,然后开始采集安卓设备摄像头的本地画面数据。同时,创建的视频 track 还会通过调用 addSink() 方法绑定视频的 VideoSink 对象实例,该对象实例在创建 PeerConnection 对象实例时通过传参设置进来的,参考代码如下:
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase.getEglBaseContext());
videoSource = factory.createVideoSource(capturer.isScreencast());
capturer.initialize(surfaceTextureHelper, appContext, videoSource.getCapturerObserver());
capturer.startCapture(videoWidth, videoHeight, videoFps);
localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
localVideoTrack.setEnabled(renderVideo);
localVideoTrack.addSink(localRender);
因此,本地摄像头视频画面数据的流向也就非常清晰了,先由 VideoCapturer 对象实例采集,然后流向视频 source,再流向视频 track,最后流向 PeerConnection 对象实例完成多媒体数据的传输。
五、SDP 协商
本地的音频数据和视频数据都已经准备好了,那么剩下的工作内容就是和远端协商传输哪些多媒体数据以及如何传输多媒体数据的问题。这自然而然就涉及到了 WebRTC 经典的 SDP 协商机制,SDP(Session Description Protocol)是会话描述协议,WebRTC 就是通过 SDP 进行协商,通过本地和远端进行 SDP 信息的交换和协商进而创建出符合通话要求的 Session,也最终决定传输通道中的数据内容。SDP 协商是 WebRTC 进行音视频通讯的基础,在整个音视频交互过程中扮演着重要角色。
1. 创建 Offer
Offer 是 WebRTC 中用来描述本地多媒体能力的 SDP 信息集合,Offer 创建的实际逻辑是在 native 层,Java 层仅仅提供了对外接口 createOffer() 方法。熟悉安卓系统开发的小伙伴一定对 JNI 非常了解,JNI 模块作为 Java 逻辑层和 native 底层的桥接层,可以轻松实现 Java 编程语言和其他编程语言的混合开发。参考代码如下:
public void createOffer(SdpObserver observer, MediaConstraints constraints) {
nativeCreateOffer(observer, constraints);
}
Offer 中的 SDP 信息会包含本地的多媒体能力,媒体描述信息的表示格式如下:
m=<媒体类型> <端口> <协议> <格式类型>
在一个会话描述中可能包含多个媒体描述,比如音频、视频、文本等。每个媒体描述都是以“m=”字段开始的,结束于下ー个“m=”,当然,也可能是到整个 SDP 会话描述结束。
其中,<媒体类型>,定义了视频、音频、文本、应用、消息这几种类型,不排除以后还会增加。<端口>,发送媒体流的端口,该字段的意义是依赖“c=”字段和<协议>字段的。
对于分层编码的码流,如果发送采用单播地址,就必须使用端口来区分这些码流。具体格式如下:
m=<媒体类型> <端口> く端口数量> <协议> <格式类型>
在这种情况下,端口的意义依赖传输协议。对于 RTP 协议来说,缺省情况下只采用偶数端口来发送 RTP 数据,对应的端口加 1 来发送 RTCP 数据。如果在“c=”字段中采用了多地址,同样,在“m=”字段中也采用了多端口,那么就认为这些地址和端口是一一对应的。
<协议>,是指传输协议,传输协议与“c=”字段相关。例如“c=”字段中的 IP4 字段就表示是在 IP4 上的协议。
如果<协议>字段是"RTP/AVP"或者"RTP/SAVP",则媒体格式表示 RTP 负载格式的编号。当出现的是一个链表的时候,表示链表中的媒体格式都可以用于当前的媒体轨,不过第一个媒体格式为缺省的格式。“a= remap:”属性是用来动态匹配媒体格式编号和媒体格式的。“a=fmtp:”属性可能用来描述媒体格式具体的参数。
如果<协议>字段是 UDP,媒体格式指定了媒体类型为音频、视频、文本、应用或者消息。这些媒体类型也就定义了对应的 UDP 传输的包格式。
媒体名称 m= (media name and transport address)
媒体标题 i=* (media title)
连接信息 c=* (connection information -- optional if included at session level)
带宽信息行 b=* (zero or more bandwidth information lines)
密钥 k=* (encryption key)
媒体属性行 a=* (zero or more media attribute lines)
下面通过一个完整的 SDP 实例来让大家看一下 SDP 具体长成什么样子:
v=0
o=- 7644049451648220451 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS ARDAMS
m=audio 44585 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
c=IN IP4 172.31.200.23
a=rtcp:9 IN IP4 0.0.0.0
a=candidate:2586587190 1 udp 2122260223 172.31.200.23 44585 typ host generation 0 network-id 3 network-cost 10
a=candidate:559267639 1 udp 2122202367 ::1 45075 typ host generation 0 network-id 2
a=candidate:1510613869 1 udp 2122129151 127.0.0.1 34137 typ host generation 0 network-id 1
a=ice-ufrag:Rcuq
a=ice-pwd:OxDSE1pHNWhgcdHaX/3cYLE1
a=ice-options:trickle renomination
a=fingerprint:sha-256 49:B6:A0:48:F8:EB:82:1D:FB:DE:B9:22:33:0E:91:EE:60:34:73:45:2B:C3:92:3A:0B:0D:FF:B1:EF:AE:8E:29
a=setup:actpass
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=sendrecv
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rt"
2. 设置 Answer
Answer 中的 SDP 信息其实是远端客户端发送过来的对端的 SDP 描述信息,和本地的 Offer SDP 描述信息相是对应的,一个是本地的,一个是远端的。但是,我们依然需要在本端设置远端的 SDP 信息,这是 SDP 协商过程中不可缺少的一环。Answer 中 SDP 信息具体字段的含义在上文已经介绍过了,这里就不赘述了。参考代码如下:
if (!signalingParameters.initiator) {
logAndToast("Creating ANSWER...");
peerConnectionClient.createAnswer();
}
注意,设置远端的 SDP 描述信息时是通过监听 onRemoteDescription() 回调事件完成的。如果你看了上面的代码,你会发现还有部分创建 Answer 描述信息的逻辑。其实,这是因为 Offer 和 Answer 的关系是相对的,如果本地作为 PeerConnection 通道建立的发起者,那么它就需要先创建 Offer,但是这个 Offer 在对端看来就是远端的 Answer。关于 Offer 和 Answer 的交换过程可以参考下图:
六、传输通道建立
WebRTC 传输通道的建立还依赖 Candidate 的设置,PeerConnection 通道建立的发起者创建并设置本地的 SDP 信息后,就会启动 ICE Candidate 的收集工作,收集的结果会通过 onIceCandidate 回调事件通知上层,然后通过调用 sendLocalIceCandidate() 方法发送给对端。对端在接收到 Candidate 后,会调用 addRemoteIceCandidate() 方法把 Candidate 绑定到对应的 PeerConnection 对象上。同样,本端也会绑定远端的 Candidate,然后,本端和远端就通过一组对应的 Candidate 完成最终的通讯。但是在这个过程中,可能还会面临由于各种原因导致的 SDP 重协商流程的发生。
WebRTC 在进行实时音视频通讯过程中需要依赖特定的多媒体数据传输通道,关于这个传输通道的建立过程本文基本上就已经介绍清楚啦。但是,本文仅仅是将一个极为简单的通道建立模型介绍了一遍,其实在整个通道的建立和使用过程中还会面临其他的很多问题,本文由于篇幅的限制没有展开,感兴趣的小伙伴欢迎评论留言沟通。
文章来源:https://xie.infoq.cn/article/b328cc6aea1bacbad57cf1360
作者简介:😄大家好,我是 Data-Mining(liuzhen007),是一位典型的音视频技术爱好者,前后就职于传统广电巨头和音视频互联网公司,具有丰富的音视频直播和点播相关经验,对 WebRTC、FFmpeg 和 Electron 有非常深入的了解,😄公众号:玩转音视频。同时也是 CSDN 博客专家、华为云享专家(共创编辑)、InfoQ 签约作者,欢迎关注我分享更多干货!😄