一个解决WebRTC在iOS模拟器红屏问题的实践

背景

随着音视频、云游戏越来越火,一个开源解决方案 WebRTC 成为了众多技术者绕不过的框架。在了解 WebRTC 基础流程后,笔者也萌生了一个想法:使用 WebRTC 实现一个点对点的视频 & 文本单聊程序。根据 WebRTC 的框架能力,视频聊天属于其基础功能。想到就做,笔者开始将想法落地。

笔者在实现点对点视频通信过程中,遇到了 iOS 模拟器红屏问题。本文主要记录如何解决这一问题。主要对 SDP 进行解读,分享在编辑 SDP 过程中遇到的问题。

环境搭建

WebRTC 虽然可以实现点对点通信,但是在其通道建立过程中,有一个信令交互环节,则必要无法完全脱离服务器,为此我们需要搭建一个信令服务器。

我们可以使用 node.js 来实现一个简单的 websocket 服务器,其只需要做一件事:广播收到的数据。我们还可以选择使用 swift 来实现这样一个简单的信令服务器。

总结一下,我们需要的环境:

  • 1)MacOS 设备,例如 Mac Mini
  • 2)信令服务器一台

遇到问题

在环境搭建完毕,工程实现完成后,急切点开应用程序的视频通话按钮后,看到的是如下图效果

模拟器红屏效果

是的,笔者遇到了模拟器上运行视频通话后红屏的问题。

这似乎是 WebRTC 在 H264 上的一个 bug,官方也发现了,所以出了一个 错误报告。该 bug 的具体原因不在本文讨论。而想要解决该问题,则有两种方案。

  • 1)修改源代码

援引 Stack Overflow 上的方案

1
Fix is to not set attribute 'kCVPixelBufferIOSurfacePropertiesKey' in  RTCVideoDecoderH264.mm for TARGET_IPHONE_SIMULATOR

笔者亲测有效。此方案的麻烦点在于,我们改动完源代码后,需要自行编译,重新生成 WebRTC.framework 文件。

  • 2)修改 SDP 内容

WebRTC 在 iOS 模拟器上会出现红屏问题,主要还是因为框架对 H264 的解码出现的 bug,我们可以通过在模拟器上不使用 H264 来规避此问题。因此,我们可以在应用层编辑 SDP 内容来达到禁用 H264 编码的目的。

SDP 是什么

想要编辑 SDP,我们首先需要了解它。SDP(Session Description Protocol),会话描述的协议,它不包含传输协议。

组成结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
                                                +---------------------+
| v= |
+---------------------+
+---------------------+ +---------------------+
==== | Session Metadata | ===== | o= |
| +---------------------+ +----------------------
| +---------------------+
| | t= |
| +---------------------+
|
|
| +---------------------+
| | c= |
| +---------------------+
| +---------------------+
==== | Network Description | =====
| +---------------------+
| +---------------------+
| | a=candidate |
| +---------------------+
|
|
| +---------------------+
| | m= |
| +---------------------+
| +---------------------+ +---------------------+
==== | Stream Description | ===== | a=rtpmap |
| +---------------------+ +----------------------
| +---------------------+
| | a=fmtp |
| +---------------------+
| +---------------------+
| | a=sendrecv.. |
| +---------------------+
+---------------+
| SEMANTIC |
| COMPONENTS OF |
| SDP |
+---------------+
| +---------------------+
| | a=crypto |
| +---------------------+
| +---------------------+ +---------------------+
==== |Security Descriptions| =====| a=ice-frag |
| +---------------------+ +----------------------
| +---------------------+
| | a=ice-pwd |
| +---------------------+
| +---------------------+
| | a=fingerprint |
| +---------------------+
|
|
|
| +---------------------+
| | a=rtcp-fb |
| +---------------------+
| +---------------------+ +---------------------+
==== | Qos,Grouping | | |
| Descriptions | =====| a=group |
+---------------------+ +----------------------
+---------------------+
| a=rtcpmux |
+---------------------+

从上文可以知道 SDP 有五个部分:

  • Session Metadata
  • Network Description
  • Stream Description
  • Security Descriptions
  • Qos, Grouping Descriptions

解读

下面根据 RTCPeerConnection.createOffer 产生的 SDP 进行解读(RFC4566)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// SDP 类型,有 offer、answer
offer
// ---------------------- Session Metadata -------------------//
// v = 0 “v =”字段给出SDP的版本,默认为0。
v=0
// o = <username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
// <username> 是用户在始发主机上的登录名
// <sess-id> <sess-id>是一个数字字符串,使得<username><sess-id>nettype><addrtype>和<unicast-address>的元组形成会话的全局唯一标识符。
// <sess-version>是此会话描述的版本号
// <nettype>是一个给出网络类型的文本字符串。 “IN”被定义为具有“Internet”的含义
// <addrtype>是一个文本字符串,给出了后面的地址类型 定义了“IP4”和“IP6”
// <unicast-address>是创建会话的计算机的地址。
o=- 4764183099742106259 2 IN IP4 127.0.0.1
// s = <会话名称> “s =”字段是文本会话名称.每个会话描述必须有一个且只有一个“s =”字段。“s =”字段不能为空
s=-
// t =<start-time> <stop-time>“t =”行指定会话的开始和停止时间。如果<stop-time>设置为零,则会话不受限制,但在<start-time>之后才会生效。如果<start-time>也为零,则会话被视为永久会话。
t=0 0
//********************* Session Metadata ********************//

//---------------------- Security Descriptions-------------------//
//a = <attribute>:<value>
//a = <fingerprint> SRIP 所需的DTLS指纹信息
a=fingerprint:sha-256 00:B3:00:58:25:4A:7D:C7:CB:E3:C6:63:43:03:71:63:33:73:CE:F9:CE:12:52:4C:95:2E:0E:96:FC:93:CE:11
// Negotiating Media Multiplexing Using the Session Description Protocol
// 表示需要共用一个传输通道传输的媒体,通过ssrc进行区分不同的流。如果没有这一行,音视频数据就会分别用单独udp端口来发送
a=group:BUNDLE 0 1 2
a=extmap-allow-mixed
// WebRTC MediaStream Identification in the Session Description Protocol
// 标识SDP中包含MediaStream的标识
a=msid-semantic: WMS stream
a=ice-ufrag:9gy+
a=ice-pwd:FlgstiDMt4ffZ2NumjV7UZPP
// Trickle ICE:Incremental Provisioning of Candidates for the Interactive Connectivity Establishment (ICE) Protocol
// ICE建立候选时 采用增量设置的方式
a=ice-options:trickle renomination
// DTLS 握手方式 "active" / "passive" / "actpass"/ "holdconn" 'actpass': 连接或启动传出连接。
a=setup:actpass
a=mid:0
//********************* Security Descriptions ********************//

//---------------------- Stream Description -------------------//
// m=<media> <port> <proto> <fmt> ...
// <media>是媒体类型 当前定义的媒体 "audio","video", "text", "application", and "message"
// <port>是传输媒体流的传输端口。在相关的“c =”字段中指定,以及在媒体字段的<proto>子字段中定义的传输协议。
// <proto>是传输协议。传输协议的含义取决于相关“c =”字段中的地址类型字段。
// <fmt>是媒体格式描述(编码类型)。第四个和任何后续 如果<proto>子字段是“RTP / AVP”或“RTP / SAVP”,则<fmt> 子字段包含RTP有效载荷类型号。
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 102 0 8 106 105 13 110 112 113 126
// c=<nettype> <addrtype> <connection-address>
// 会话描述必须包含 每个媒体描述中的至少一个“c =”字段或会话级别的单个“c =”字段
// <nettype> “IN”被定义为具有“Internet”的含义
// <addrtype> 为IP4和IP6时
c=IN IP4 0.0.0.0
// The URI for declaring this header extension in an extmap attribute is "urn:ietf:params:rtp-hdrext:ssrc-audio-level".
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
//a = sendrecv 这指定应以发送和接收模式启动工具。对于具有默认为仅接收模式的工具的交互式会议,这是必需的。
a=sendrecv
a=msid:stream audio0
// "a = rtcp-mux"属性以指示需要RTP和RTCP多路复用
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=ssrc:3316495333 cname:FlAaGq79STLBNArX
a=ssrc:3316495333 msid:stream audio0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 35 36 123 122 125
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:9gy+
a=ice-pwd:FlgstiDMt4ffZ2NumjV7UZPP
a=ice-options:trickle renomination
a=fingerprint:sha-256 00:B3:00:58:25:4A:7D:C7:CB:E3:C6:63:43:03:71:63:33:73:CE:F9:CE:12:52:4C:95:2E:0E:96:FC:93:CE:11
a=setup:actpass
a=mid:1
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:stream video0
a=rtcp-mux
a=rtcp-rsize
// rtpmap:<payload type> <encoding name> / <clock rate> [/ <encoding parameters>]
// <payload type> 此属性从RTP有效内容类型编号(在 “m =”行中使用)映射到表示要使用的有效载荷格式,编码类型 采样率 编码参数
// 而以下格式的编码的格式要去相应编码格式标准文档中查看 比如H264 就是RFC3984 https://tools.ietf.org/html/rfc3984
a=rtpmap:96 H264/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
// a = rtcp-fb: RTCP-FB-PT SP RTCP-FB-VAL CRLF
// rtcp-fb-pt是有效负载类型
// rtcp-fb-val定义反馈消息的类型 ack,nack,trr-int和rtcp-fb-id
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 H264/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:125 ulpfec/90000
a=ssrc-group:FID 2531124184 1348659893
a=ssrc:2531124184 cname:FlAaGq79STLBNArX
a=ssrc:2531124184 msid:stream video0
a=ssrc:1348659893 cname:FlAaGq79STLBNArX
a=ssrc:1348659893 msid:stream video0
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:9gy+
a=ice-pwd:FlgstiDMt4ffZ2NumjV7UZPP
a=ice-options:trickle renomination
a=fingerprint:sha-256 00:B3:00:58:25:4A:7D:C7:CB:E3:C6:63:43:03:71:63:33:73:CE:F9:CE:12:52:4C:95:2E:0E:96:FC:93:CE:11
a=setup:actpass
a=mid:2
a=sctp-port:5000
a=max-message-size:262144

媒体流信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// m=<media> <port> <proto> <fmt> ...
// <media>是媒体类型 当前定义的媒体 "audio","video", "text", "application", and "message"
// <port>是传输媒体流的传输端口。在相关的“c =”字段中指定,以及在媒体字段的<proto>子字段中定义的传输协议。
// <proto>是传输协议。传输协议的含义取决于相关“c =”字段中的地址类型字段。
// <fmt>是媒体格式描述(编码类型)。第四个和任何后续 如果<proto>子字段是“RTP / AVP”或“RTP / SAVP”,则<fmt> 子字段包含RTP有效载荷类型号。
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 124 35 36 123 122 125
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:9gy+
a=ice-pwd:FlgstiDMt4ffZ2NumjV7UZPP
a=ice-options:trickle renomination
a=fingerprint:sha-256 00:B3:00:58:25:4A:7D:C7:CB:E3:C6:63:43:03:71:63:33:73:CE:F9:CE:12:52:4C:95:2E:0E:96:FC:93:CE:11
a=setup:actpass
a=mid:1
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:stream video0
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 H264/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 H264/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:127 VP9/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtpmap:124 rtx/90000
a=fmtp:124 apt=127
a=rtpmap:35 AV1/90000
a=rtcp-fb:35 goog-remb
a=rtcp-fb:35 transport-cc
a=rtcp-fb:35 ccm fir
a=rtcp-fb:35 nack
a=rtcp-fb:35 nack pli
a=rtpmap:36 rtx/90000
a=fmtp:36 apt=35
a=rtpmap:123 red/90000
a=rtpmap:122 rtx/90000
a=fmtp:122 apt=123
a=rtpmap:125 ulpfec/90000
a=ssrc-group:FID 2531124184 1348659893
a=ssrc:2531124184 cname:FlAaGq79STLBNArX
a=ssrc:2531124184 msid:stream video0
a=ssrc:1348659893 cname:FlAaGq79STLBNArX
a=ssrc:1348659893 msid:stream video0

解决方案

上文中提到修改源代码来解决红屏问题。另外一种方式就是修改 SDP 内容,而 SDP 的内容修改,也有两个方法。其一是修改底层源代码生成 offer 的实现,将 H264 在模拟器环境下排除。另一种则是在应用层修改 SDP 内容。

在实际修改过程中,很可能会遇到 Session Description is NULL. 报错。相信笔者,我们的实现思路是没有错的,只要再注意一些小细节即可。

  • 1)在 RTCPeerConnection.setRemoteDescription 前修改 SDP 内容。
  • 2)注意 \r\n 符号。

我们需要确保将上文示例 SDP 内容修改如下即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
m=video 9 UDP/TLS/RTP/SAVPF 100 101 127 124 35 36 123 122 125
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:9gy+
a=ice-pwd:FlgstiDMt4ffZ2NumjV7UZPP
a=ice-options:trickle renomination
a=fingerprint:sha-256 00:B3:00:58:25:4A:7D:C7:CB:E3:C6:63:43:03:71:63:33:73:CE:F9:CE:12:52:4C:95:2E:0E:96:FC:93:CE:11
a=setup:actpass
a=mid:1
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:stream video0
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:100 VP8/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:127 VP9/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=rtpmap:124 rtx/90000
a=fmtp:124 apt=127
a=rtpmap:35 AV1/90000
a=rtcp-fb:35 goog-remb
a=rtcp-fb:35 transport-cc
a=rtcp-fb:35 ccm fir
a=rtcp-fb:35 nack
a=rtcp-fb:35 nack pli
a=rtpmap:36 rtx/90000
a=fmtp:36 apt=35
a=rtpmap:123 red/90000
a=rtpmap:122 rtx/90000
a=fmtp:122 apt=123
a=rtpmap:125 ulpfec/90000
a=ssrc-group:FID 2531124184 1348659893
a=ssrc:2531124184 cname:FlAaGq79STLBNArX
a=ssrc:2531124184 msid:stream video0
a=ssrc:1348659893 cname:FlAaGq79STLBNArX
a=ssrc:1348659893 msid:stream video0

效果展示

模拟器红屏效果修复

文中图片若有侵权,请联系笔者及时删除