第一次看到 WebRTC 这个词是 15 年在一期 Android Weekly 中,但当时完全看不懂它在讲什么,也就没有深究。两年后,我开始搞起 WebRTC,并整理出了一套开箱即用的 WebRTC 开发环境,距今又过了一年多。
按常理,这篇文章要介绍的内容本应最先呈现,但我搞 WebRTC 的路线略微反常,由于没有实际使用的需求,所以我先是研究了不少模块的实现原理。但随着接触到更多内容,我越来越意识到,如果没有一个清晰的全局观,那效率将会越来越低,因此也就有了这篇文章。
鉴于目前我对 WebRTC 的了解仍然有限,所以本文很可能仍有遗漏,或错误,但我会持续更新本文。
SDP 基本结构
首先我们搞清楚 SDP 的基本结构。
总体来说,WebRTC 的 SDP 分为几个部分:
- session metadata: v=, o=, s=, t=
- network description: c=, a=candidate
- stream description: m=, a=rtpmap, a=fmtp, a=sendrecv …
- security description: a=crypto, a=ice-frag, a=ice-pwd, a=fingerprint
- QoS, grouping description: a=rtcp-fb, a=group, a=rtcpmux
m= 开头的一段叫做 m section,这一行叫 m line,里面有很多 a line 来描述这种 media 的各种属性。我们称一种媒体数据为一种 media,每种 media 在 SDP 里都有 m section。
WebRTC 的 SDP 有三种类型:
- offer: 发起方提供的自己对本次通话的描述;
- answer: 其他方收到 offer 后,给出的回应;
- pranswer: provisional answer,非最终 answer,之后可能被 pranswer 或 answer 更新;
Plan B v.s. Unified Plan
说到 SDP,就不得不提它的两种 Plan,它们是表达传输多路媒体流时的两种 SDP 格式。多路媒体流的例子有:录屏 + 相机,或多个相机(视角)。
Plan B 是 SDP 里同类型的媒体流只有一个 m line,同类型的多个媒体流之间通过 msid 区分,而 Unified Plan 则是每个媒体流都有一个 m line,因此如果有两路视频,那就会有两个 video m line。
WebRTC 标准采纳的是 Unified Plan,WebRTC 代码也已支持,所以我们就只关注 Unified Plan 的 API。
参考:Plan B, Unified Plan, Unified Plan vs Plan B。
Plan B 在 WebRTC 源码里对应的是 PC 的 Stream/Sender/Receiver API,Unified Plan 对应的是 Track/Transceiver API。
Capturer/Source/Track/Sink/Transceiver
接着我们梳理一下媒体数据交换过程中的几个关键概念:
- Capturer: 负责数据采集,只有视频才有这一层抽象,它有多种实现,相机采集(Android 还有 Camera1/Camera2 两套)、录屏采集、视频文件采集等;
- Source: 数据源;
- Track: 媒体数据交换的载体,发送端把本地的 Track 发送给远程的接收端,每个 Track 都有自己的 track id,多个关联的 Track 有一个相同的 stream id;
- Sink: Track 数据的消费者,只有视频才有这一层封装,发送端视频的本地预览、接收端收到远程视频后的渲染,都由 Sink 负责;
- Transceiver: 负责收发媒体数据;
以视频为例,数据由发送端的 Capturer 采集,交给 Source,再交给本地的 Track,然后兵分两路,一路由本地 Sink 进行预览,一路由 Transceiver 发送给接收端;接收端 Track 则把数据交给 Sink 渲染。
Capturer 的创建和销毁完全由 APP 层负责,只需要把它和 Source 关联起来即可;创建 Source 需要调用 PC Factory 接口,创建 Track 也是,并且需要提供 Source 参数;Sink 的创建和销毁也由 APP 层负责,只需要把它们添加到 Track 里即可;创建 Transceiver 则需要调用 PC 接口。
好了,接下来我们就看看 PC Factory 和 PC 的接口。
PeerConnectionFactory 接口
CreatePeerConnectionFactory
默认的编译选项里,rtc_use_builtin_sw_codecs = false
,因此 USE_BUILTIN_SW_CODECS
未被定义,CreatePeerConnectionFactory
只有一个重载版本:接收三个 thread、adm、audio/video encoder/decoder factory、AudioMixer 和 AudioProcessing。
CreatePeerConnection
创建 PC 对象,接收 RTCConfiguration 和 PeerConnectionDependencies,前者用来容纳各种配置,后者则用来容纳各种可定制的接口实现,例如 PortAllocator, AsyncResolverFactory, RTCCertificateGeneratorInterface, SSLCertificateVerifier。
目前 Android/iOS 对 dependencies 的支持还未跟上,虽然这种高级用法的用户也不怕在 native 层自己做封装,但就又得重新造一遍 WebRTC Java/ObjC 代码里的轮子了。
CreateAudio/VideoSource/Track
这就是前面我们提到的创建 Audio/Video Source/Track 的接口了。
PeerConnection 接口
准备工作相关:
- AddTrack: 添加要发送的 track;
- AddTransceiver: 添加 transceiver;
- CreateDataChannel: 添加 DataChannel;
- RemoveTrack: 移除 track;
- GetTransceivers: 获取所有的 transceiver;
建立 P2P 连接相关:
- CreateOffer: 创建 offer;
- CreateAnswer: 创建 answer;
- SetLocalDescription: 设置本地 SDP;
- SetRemoteDescription: 设置远端 SDP;
- AddIceCandidate: 添加 ICE candidate;
- RemoveIceCandidates: 移除 ICE candidate;
注意:CreateOffer/CreateAnswer 时传入的 RTCOfferAnswerOptions 里,有 offer_to_receive_X
字段,它们是为了兼容 Plan B 语义的,一旦设置,即便没有 AddTrack,SDP 里也会包含 audio/video 的 m line。使用 Unified Plan 时,不应设置这两个字段,而应提前调用 AddTrack/AddTransceiver/CreateDataChannel,来表明自己是否需要 audio/video/data。
其他接口:
- GetStats: 获取统计数据;
- SetBitrate: 设置这个 PC 总的发送码率,包括初始码率、最小码率、最大码率;
- SetBitrateAllocationStrategy: 设置自定义码率分配策略,可以通过这个接口实现针对每个 track 的码率分配策略;
注意:SetBitrateAllocationStrategy
在 Android 和 iOS 平台都没有暴露出来,Android 暴露了 SetBitrate 接口,iOS 则没有,不过可以通过 RTCRtpSender setParameters
限制编码器的输出码率。
回调接口 PeerConnectionObserver:
- OnSignalingChange: 产生/设置 SDP 后,会触发 signaling state 变化,常见的变化是
stable -> have-local-offer -> stable
或stable -> have-remote-offer -> stable
,具体可以查看 SPEC 4.3 State Definitions; - OnRenegotiationNeeded: 需要重新协商(重新建立 P2P 连接)时回调,例如 ICE restart 时会回调;
- OnIceGatheringChange: ICE candidate 收集状态变化后回调;
- OnIceConnectionChange: ICE 连接状态变化后回调;
- OnIceCandidate: 收集到本地 ICE candidate 后回调;
- OnIceCandidatesRemoved: 本地 ICE candidate 被移除后回调;
- OnTrack: 调用 SetRemoteDescription 后,如果 SDP 表明将会创建接收用的 transceiver,就会回调这个接口;
- OnRemoveTrack: 当确定一个 track 不再接收媒体数据后,会回调这个接口,track 不会移除,但 transceiver 的 recv 方向将会被去掉;
接下来我们重点看一下 transceiver。
RtpTransceiver
SDP 的 m section 里有一行 a=mid:
,定义了这种 media 的 id,叫 mid,例如下面这对 offer 和 answer:
# offer
...
a=group:BUNDLE 0 1 2
...
m=video 9 UDP/TLS/RTP/SAVPF 100 96 97 98 99 101 127 124 125
...
a=mid:0
...
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
...
a=mid:1
...
m=application 9 DTLS/SCTP 5000
...
a=mid:2
...
# answer
...
a=group:BUNDLE 0 1 2
...
m=video 9 UDP/TLS/RTP/SAVPF 100 96 97 98 99 101 127 124
...
a=mid:0
...
m=audio 9 UDP/TLS/RTP/SAVPF 103 104 9 102 0 8 106 105 13 110 112 113 126
...
a=mid:1
...
m=application 9 DTLS/SCTP 5000
...
a=mid:2
...
其中有三种 media: video, audio, application,mid 依次为 0, 1, 2。application 是 DataChannel 的 media type。
我们注意到,offer 和 answer 里同一种 media 的 mid 是相同的,也就是说,对某一端来说,他收发的同一种媒体数据,mid 是相同的。
在 WebRTC 标准里,transceiver 表示的就是收发相同 mid 的 sender 和 receiver 的一个组合体,其中会有 media type, mid, direction, sender, receiver 等字段。其中 direction 有几种取值:kSendRecv, kSendOnly, kRecvOnly, kInactive。
AddTrack 时我们 add 的是本地的 track,即要发送的数据流,首次 AddTrack 时,会创建 transceiver,默认其 direction 是 kSendRecv。尽管在 CreateOffer 时我们可以通过设置 RTCOfferAnswerOptions
的 offer_to_receive_X
字段来控制是否 receive,但这两个字段是 legacy 字段,我们应该尽量避免。那如何控制 transceiver 的方向呢?我们可以使用 AddTransceiver 接口。
如果想要创建 kSendOnly 的 transceiver,可以传入 track,并在 RtpTransceiverInit
中设置 direction 为 kSendOnly;或者只传入 media type 和 init 结构体,稍后再 AddTrack。如果想要创建 kRecvOnly 的 transceiver,可以只传入 media type 和 init 结构体,并且不 AddTrack。
transceiver 何时与 SDP 里的 m section 关联呢?offer 端在创建 offer 时,会根据已有的 transceiver 创建 m section,并记下每个 transceiver 在 SDP 里对应的 m section 的 index 值,以便在 SetLocalDescription 时,可以为 transceiver 设置正确的 mid;answer 端在 SetRemoteDescription(offer 端发来的 offer)时,如果 offer 里的 m section 有 recv 方向,那就按 media type 来查找已有的 transceiver,如果能找到就可以将其关联起来,否则就创建一个 kRecvOnly 的 transceiver(因为 offer 只有可能是 kSendOnly 了,不发也不收的 media,不会出现在 SDP 里,那对此 offer 的回应也就只能是 kRecvOnly 了)。
总结一下,无论是 offer 端还是 answer 端,需要发送的 media,才提前添加好有 send 方向的 transceiver,仅接收的 media,无需提前添加 transceiver(提前添加了也不会被使用)。
附录一:SDP 部分细节
- m line 里会指明传输协议,例如 UDP/TLS/RTP/SAVPF,最后的 SAVPF 还有其他几种值:AVP, SAVP, AVPF, SAVPF
- AVP 意为 AV profile
- S 意为 secure
- F 意为 feedback
- rtpmap 是描述 codec 的,但有特殊的 rtx codec,其实不是 codec,例如 rtx;
- fmtp 补充描述 codec 的参数,format parameters
- max-fr: maximum framerate
- profile-level-id: H.264 的 profile level id
- rtx 描述重传策略,由 rtpmap 指明,它的参数由 fmtp 描述
- apt: associated payload type,指明所描述的 stream;
- rtx-time: rtp 包在缓冲区保留时间;
- rtcp-fb: RTCP 反馈机制
- offer 里面列出一些反馈机制,answer 里应移除不理解、不支持的机制,但不能修改;
- ack rpsi/app
- nack pli/sli/rpsi/app
- rpsi: reference picture selection indication
- app: app 层反馈机制
- pli: picture loss indication,表明收流端丢失了一幅图像的一些数据,发送端可能会发送一个 I 帧(类似于 FIR),但要考虑拥塞控制
- sli: slice loss indication
- ccm fir: codec control message, full intra refresh
- fec 类似于 rtx,也由 rtpmap 指明,它的参数由 fmtp 指明;
欢迎大家加入 Hack WebRTC 星球,和我一起钻研 WebRTC。