FaceTime终直面WebRTC:deep dive实现(上)

在今年的WWDC演讲中,苹果宣布FaceTime在web浏览器中可用,同时支持Android和Windows用户端。我们上次研究FaceTime还是六年前(详见此篇文章),现在是时候更新了。FaceTime肯定使用了WebRTC,这篇文章中我会给大家解释为什么很大程度上能确定它使用了WebRTC。

摘要

FaceTime Web确实在媒体服务中使用了WebRTC,并使用Insertable Streams API进行端对端加密。它还使用了一种有趣的方法来避免simulcast。

感谢Dag-Inge Aas抽出时间组织了一次会议,帮助我利用必要数据进行分析。那之后我的工具更加实用了。所以除了WebRTC内部转储之外,我还从Chrome日志中获取了RTP转储和SCTP转储,以及一些进行端对端加密(E2EE)的JavaScript。

加入会议

当我把邀请链接粘贴到浏览器上时,它要求我输入名字,然后加入会话。但接入需要由发起会话的人接受才行。另外到目前为止,好像还不能直接从web上发起会话。

设备测试急需能显示音频状态的仪器,我们实在要花太多时间才能弄清楚,是谁的麦克风没有正常工作。

在由WebSocket发送的二进制信号流量中,我可以找到Dag-Inge的电话号码。毕竟他的号码对我来说并不陌生。但你一定要谨慎决定给谁分享会话邀请。

URL

首先,让我们从Dag-Inge发送的URL开始。

https://facetime.apple.com/join#v=1&p=<a base64 encoded string>&k=<another base64 encoded string>

URL中hash部分的使用是意料之中的,毕竟去年Jitsi Meet中也使用了同样的技术进行端到端加密。hash的重要特性是它不会被发送到服务器上,所以成为了放置敏感数据(如加密密钥)的好地方。

p参数(“p” argument)会通过安全的WebSocket发送到服务器上,所以这很可能是一个公钥。接下来我们将进一步介绍端对端加密。

webrtc-internals 分析

如果你想自己分析得出结论,可以在这里找到转储,然后使用导入工具导入。

getUserMedia 约束

对于webrtc-internals转储,getUserMedia部分比较容易解释。getUserMedia用于获取麦克风和摄像头允许。视频约束要求具备面向用户的摄像头,和720像素的理想清晰度,没有指定宽度,但对于大多数相机来说,该要求最终捕获到的都是1280×720像素的视频流。之后通过使用track.applyConstraints方法,再将其裁剪为720×720的正方形图像。

RTCPeerConnection 参数

RTCPeerConnection部分比较复杂。构造函数的参数实际表明,端到端加密中确实使用了Insertable Streams(现名为Encoded Transform)。

Connection:21-1 URL: https://facetime.apple.com/applications/facetime/current/en-us/index.html?rootDomain=facetime#join/

Configuration: "{

iceServers: [],

iceTransportPolicy: all,

bundlePolicy: max-bundle,

rtcpMuxPolicy: require,

iceCandidatePoolSize: 0,

sdpSemantics: \"unified-plan\",

encodedInsertableStreams: true,

extmapAllowMixed: true

}

"Legacy (chrome) constraints: ""

Signaling state: => SignalingStateHaveLocalOffer => SignalingStateStable => SignalingStateHaveRemoteOffer => SignalingStateStable => SignalingStateHaveRemoteOffer => SignalingStateStable => SignalingStateHaveRemoteOffer => SignalingStateStable

ICE connection state: => checking => connected

Connection state: => connecting => connected

这里的统一计划sdpSemantics是默认的(如果你采用的仍然是其他方法,建议你尽快停止这样做)。rtcpMuxPolicy被设置为require,max-bundle的bundlePolicy也很标准。encodedInsertableStreams设置为true,是为了启用Insertable Streams API。它只在Chrome92或更高版本的[chrome://webrtc-internals]中显示。

该应用没有使用ICE服务器。这意味着如果UDP被屏蔽,一切都无法继续了。鉴于这种连接只与服务器对话,很多非限制性环境(比如很多企业)通常不使用ICE服务器。这样有限的传输选项,使得FaceTime不太可能成为[The Verge]所说“Zoom真正的竞争对手”。至少商业用例是如此。

Offer及answer交流

高级别的WebRTC流程如下所示。

一开始,客户端向服务器提供一个数据通道,然后服务器发送一个新的offer来添加音频和视频。在每个步骤中,添加到SDP中的媒体部分的数量(2、7、12…)是相当重要的,稍后会做解释。

SDP 分析

第一个纯数据通道offer的answer是非常标准的(点击此处,查看之前在SDP研究文章中所述关于行的含义)。

v=0

o=- 6972089255627587584 6972089255627587584 IN IP4 127.0.0.1

s=-

t=0 0

a=group:BUNDLE 0

a=ice-lite

m=application 9 UDP/DTLS/SCTP webrtc-datachannel

c=IN IP4 127.0.0.1

a=rtcp:9 IN IP4 0.0.0.0

a=candidate:1 1 udp 1 17.252.108.36 3483 typ host

a=ice-ufrag:iiTRM3DatXbD

a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM

a=ice-options:trickle

a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F

a=setup:passive

a=mid:0

a=sctp-port:5000

a=max-message-size:262144

a=rtcp-mux

a=rtcp-rsize

然而,它确实在没有任何意义的部分指定了[rtcp]、[rtcp-mux]和[rtcp-rsize]属性。此举并不明智。

ice-options是一个会话级别的属性,不属于媒体。这是由[WebRTC发展产生]的一个bug。

服务器是一个[ice-lite服务器],所以没有端对端的连接。但Dag-Inge和我会以1-1的方式开始会话,之后才由他那边的第二个设备加入。这种使用直接端对端连接的会话模式通常被称为[P2P4121],相当常见,但本篇文章不会涉及。

特别需要注意的一点是:控制DTLS 握手方向的设置属性为被动。这一点在我们观察统计数字时很重要。

视频参数

重点在于来自服务器的第一个offer。它在数据通道部分没有变化,而且服务器为每位参与者增加了三个视频部分和两个音频部分。

m=video 9 UDP/TLS/RTP/SAVPF 123

c=IN IP4 127.0.0.1

a=rtcp:9 IN IP4 0.0.0.0

a=candidate:1 1 udp 1 17.252.108.36 3483 typ host

a=ice-ufrag:iiTRM3DatXbD

a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM

a=ice-options:trickle

a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F

a=setup:passive

a=mid:1

a=extmap:1 urn:3gpp:video-orientation

a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time

a=recvonly

a=rtcp-mux

a=rtcp-rsize

a=rtpmap:123 H264/90000

a=rtcp-fb:123 nack pli

a=rtcp-fb:123 ccm fir

a=rtcp-fb:123 goog-remb

a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42c01f

不出所料,视频部分使用了H.264基线配置文件42c01f作为唯一可用的编解码器。它们差别不大,且显示为recvonly,用于表明收发器是单向使用的。

[RTX]不用于重传,且我们能看到urn:3gpp:video-orientation扩展(它允许指定视频方向,这一点对于移动设备相当重要。因为这些设备在通话过程中的位置往往会发生变化)以及abs-send-time扩展。后者与我2013年所述,至今仍然流行的[REMB带宽估计算法]一起使用。较新的(2015年)[transport-cc算法和扩展]还没有被启用。感谢Harald促成了苹果在SDP中加“goog-remb”这一举措。

我们从视频部分的offer中领会的最后一点是:它缺少一个[nack反馈机制](只有“nack pli”。对于WebRTC库来说可能没什么区别)。

音频参数

另一方面,两个音频m-line也很值得探讨。

m=audio 9 UDP/TLS/RTP/SAVPF 96

c=IN IP4 127.0.0.1

a=rtcp:9 IN IP4 0.0.0.0

a=candidate:1 1 udp 1 17.252.108.36 3483 typ host

a=ice-ufrag:iiTRM3DatXbD

a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM

a=ice-options:trickle

a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F

a=setup:passive

a=mid:4

a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level vad=off

a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time

a=recvonly

a=rtcp-mux

a=rtcp-rsize

a=rtpmap:96 opus/48000/2

a=rtcp-fb:96 goog-remb

a=fmtp:96 ptime=60

m=audio 9 UDP/TLS/RTP/SAVPF 97

c=IN IP4 127.0.0.1

a=rtcp:9 IN IP4 0.0.0.0

a=candidate:1 1 udp 1 17.252.108.36 3483 typ host

a=ice-ufrag:iiTRM3DatXbD

a=ice-pwd:vAsEpbbiFr7whr1KxJlwGKiM

a=ice-options:trickle

a=fingerprint:sha-256 76:DD:71:C3:27:74:F6:2D:38:77:1B:C2:A5:8C:26:3E:9D:B4:21:C7:A1:BE:03:CE:E7:A5:75:99:F9:1D:B4:8F

a=setup:passive

a=mid:5

a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level vad=off

a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time

a=recvonly

a=rtcp-mux

a=rtcp-rsize

a=rtpmap:97 opus/48000/2

a=rtcp-fb:97 goog-remb

a=fmtp:97 ptime=40;useinbandfec=1

Opus编解码器用于音频。它一定是人为添加到本地FaceTime客户端的(以前没有)。Natalie Silvanovich可能会像以前一样,研究一下此处潜在的漏洞,供我们探讨。

安装了iOS 15(但不支持E2EE方案)的设备才能加入通话,也说明了上述情况。

但我不得不说,我不理解把指纹、ice-ufrag和ice-pwd安排到每个媒体部分,而不是会话里的操作。这使相同的信息重复了许多次。

另外,这个offer的设置属性是被动的(来自之前的answer)。这直接违反了[RFC 5763]。奇怪的是,Chrome浏览器接受了该操作。这时就需要[IETF协议]来解决问题了。

说到header的扩展,我们观察到ssrc-audio-level有一个额外的指定符vad=off和abs-send-time扩展。ssrc-audio-level通常用于SFU中的[主动式扬声器检测],FaceTime就在使用这种功能。

这意味着FaceTime会发送未加密的音频级数据。这是种效果不太理想但却很常见的做法。在Lennart Grahls对WebRTC的开发贡献中,我们可能会找到修复加密header扩展的方法。

使用vad=off在技术上是允许的。但这个功能很鸡肋,WebRTC库甚至没有实现它。如果你认为大有可为,那可以星标这个bug,但不要期望这个问题有什么进展。在文章的JavaScript部分我们会再次看到这个问题。

这里两部分音频的使用很有趣。他们有不同的编解码器规格。也都有Opus,但有效载荷类型(96和97)不同,打包时间一个是40毫秒,一个是60毫秒,比WebRTC通常使用的20毫秒大得多。此外,[Opus inband FEC]只对其中一部分音频启用。这可能是因为使用了两种不同有效载荷类型,并没有使用Opus–DTX–不连续传输。

如果通话是端对端加密的(这点并未说明,但使用 insertable streams 来提供另一加密层已经表明了这一点。希望FaceTime未来能发布一份像[Google Duo]那样详尽的白皮书。)

服务器的下一个offer是再增加五个媒体部分。此处有两点值得注意:

首先,新的媒体部分是sendonly,表明有新的参与者加入了通话。其次,FaceTime相当微妙地在每个部分增加了带有cname的a=ssrc行(不清楚他们为什么这样做。但这意味着远程端现在可以使用这些SSRCs发送数据。具体位置如下:

a=ssrc:3633224106 cname:3243262565732983040

a=ssrc:2993385163 cname:3243262565732983040

a=ssrc:4150121997 cname:3243262565732983040

a=ssrc:1095248263 cname:3243262565732983040

a=ssrc:2285848791 cname:3243262565732983040

很明显,这几行代码的cname相同。表明它们来自同一个参与者解码[RFC 3550语言]即可得出该结论)。这有点像一个不太理想的simulcast,但实际上是一个相当有趣的黑客操作。这一点会在揭秘JavaScript时详述。

这些cnames似乎都是数字,其中一个甚至是负数,这样的选择很奇怪。

其余的WebRTC API调用就是常规操作了——在第三个设备加入时又增加了五个媒体部分。

下面我们来看转储的数据部分。

WebRTC 数据

chrome://webrtc-internals收集的数据信息向我们展示了很多应用程序相关的行为,比如比特率。

该数据也能告诉我们一些加密相关的信息。我们能看到DTLS交换中使用的证书种类(即现行标准ECDSA证书)和SRTP密码,即AEAD_AES_256_GCM,这是一个去年在Chrome中启用的密码套件。因为它不是默认的密码套件,所以只在某些情况下启用。这也解释了SDP中使用被动设置的原因,详情请见chrome bug。看来FaceTime的开发者也读过bug这篇文章。或者说他们也具备文章中的这些知识。

在传入的音频数据中,我们可以看到适量的前向纠错(FEC),没有丢包。

虽然Opus FEC在冗余方面的表现不如RED好,但它确实提升了低丢包情况下的弹性。数据包率低于通常的每秒50个。但这可以理解。因为帧比较大(40和60ms。并不是20ms;且较大的帧增加了延迟,降低了成本)。传入的比特率约为50 kbps,比传出的30 kbps带宽略高。

下图表明,每次只能发送一个音频SSRCs,说明有一些类似于音频通话的行为出现。

稍后我们会在RTP转储中说明这一点。

在下图中,我们可以看到类似视频通话的行为。

通常此处是simulcast,但图中传递给我们的信息并不是这样。比如分辨率方面,我们看到了三种不同的分辨率——192×192、320×320和720×720。带宽估算缓慢上升的同时,流从低分辨率流(中间位置)升至中等分辨率(顶部位置),最后到达720×720层这一顶端,连接稳定,带宽大约750 kbps。

通常情况下,上图操作是使用simulcast完成的。FaceTime的开发者似乎有意避开这一做法,转而使用JavaScript级别的黑客完成这一操作。

我们可以看到,用于出站流的分辨率与入站流相同。转储中的比特率表现不太正常,表明在通话期间有相当长的时间没有发送视频。当然,与任何新的设置一样,我们找到了可能能解释这种行为的操作。

高分辨率流的目标比特率差不多是800 kbps左右,320×320流是200 kbps,192×192缩略图是60 kbps。这样的流只会在通话的特定阶段(比如当第三个设备加入时?)被发送,之后就变成就会同其他流一起发送了。

webrtc-internals没有跟踪RTCRtpSender.setParameters调用的功能,该功能可以让大家更深入了解获得这种行为所需的高级操作。希望能有人无偿提供一个补丁。

看过统计数据后,我们接着讨论JavaScript。

文章地址:https://webrtchacks.com/facetime-finally-faces-webrtc-implementation-deep-dive/

原文作者:Philipp Hancke

推荐阅读
作者信息
jemmayb
TA 暂未填写个人简介
文章
27
相关专栏
开源技术
74 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和 Agora 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。