文章目录
webrtc推荐教程:
免费FFMPEG/srs/WEBRTC课程
SDP结构
SDP 描述分为两部分,分别是会话描述(session level)和媒体描述(media level),其具体的组成可参考 RFC4566,带星号 (*) 的是可选的。常见的内容如下:1-3行是会话描述、第四行是媒体描述。
在整个SDP种,只能有一个会话描述,而媒体描述可以有多个。
通常SDP种包含两个媒体描述:
- 音频媒体描述
- 视频媒体描述
除了话描述是对整个SDP起约束作用外,各媒体描述之间的约束互不影响。
v=0
o=- 1954504395161900476 2 IN IP4 127.0.0.1
...
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
...
SDP的描述格式:
<type> = <value>
其中type描述描述的目标,它有单个字符组成;value是对type的解释或约束。
会话描述
会话包含的类型有
- v:协议版本(protocol version),一般为0
- o:会话的创建者(owner/creator and session identifier)
- s:会话名(session name)
- t:会话时长(time the session is active)
会话创建者o
o=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
o=- 1954504395161900476 2 IN IP4 127.0.0.1
o=字段给出了会话的发起者(用户名和用户的主机地址)加上会话标识符和版本数量
username:用户登录是否在原始主机上,如果为-表示不支持用户id的概念,不能包含空格。
sess-id:会话ID
sess-version:会话的版本
nettype:网络类型
addrtype:地址类型,通常为IP4、IP6
unicast-address:发起者的地址,webrtc中并不适用这个
媒体描述
媒体信息
属于标准SDP中媒体描述的内容,同时也是SDP中最核心的内容。其最重要的是“m=”行描述。在“m=”行中描述了媒体类型、传输类型、PayloadType等信息。对于每种媒体数据(音频数据、视频数据),可以选择多种编解码器(Opus、iLBC、H264……)对其进行编解码。每种编解码器的详细参数可以通过“a=rtpmap”属性进一步解释。
m=<media><port> <proto><fmt> ...
//offer audio
//媒体类型是音频,采用SAVPF传输流媒体数据,且RTP包的类型可能是111、103...126
//传输底层使用UDP,在UDP之上使用了DTLS协议来交换证书,证书交换好后。
//媒体数据由RTP进行传输(RTP运行在UDP之上),保证传输的可靠性
//媒体数据的安全性是由SRTP负责的,即对RTP包中的bodyv部分进行加密。
//此外传输时还是用RTCP的feekback机制对传输信息进行实时反馈(SAVPF),以便进行拥塞控制。
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
//offer video
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
//answer audio
m=audio 9 UDP/TLS/RTP/SAVPF 111
//answer video
m=video 9 UDP/TLS/RTP/SAVPF 125 114
- media:媒体类型。包括 video、audio、text、application、message等。
- port:传输媒体流的端⼝。对于webrtc而言由于它不适用SDP中描述的网络信息,所以该端口号对它没任何意义。
- proto:传输协议,具体含义取决于c=中定义的地址类型,⽐如c=是IP4,那么这⾥的传输协议运⾏在IP4之上。⽐如:
- fmt:媒体格式的描述,可能有多个。根据 proto 的不同,fmt 的含义也不同。⽐如 proto 为RTP/SAVP 时,fmt 表示 RTP payload 的类型。如果有多个,表示在这次会话中,多种payload类型可能会⽤到,且第⼀个为默认的payload类型。
对于RTP/SAVP,payload type又分为两种类型:
- 静态类型
- 动态类型:在
a=fmtp
中定义
音频媒体信息
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
a=mid:0
...
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=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:106 CN/32000
a=rtpmap:105 CN/16000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
第三行a=mid:0
表示音频媒体的ID编号为0,a=mid 属性可以认为是每个m=描述的唯⼀ ID。⽐如 a=mid:audio,那么 audio 这个字符串就是这个 m=描述的 ID。有的时候 mid 属性值也可以⽤数字表示,⽐如 a=mid:0,那么 0 也是 这 个m=描 述 的 ID 。 mid 值⼀般和 grouping 传输属性的 BUNDLE 策略结合来⽤,⽐如a=group:BUNDLE audio video,代表本次会话将对 mid 为 audio 和 video 的 m=描述进⾏复⽤传输。
a=rtpmap
a=rtpmap:<payload type> <endcoding name>/<clock rate>/[/<encodingparameters>]
rtpmap是一个payload type与编码器的映射表
通过rtpmap的格式我们可以很容易理解这个a=rtpmap:111 opus/48000/2
的含义:
- payload type:111
- 编解码器(endcoding name):opus
- 时钟频率即采样率(clock rate):48000
- 音频通道数(encodingparameters):2
a=fmtp
a=fmtp:<format> <format specific parameters>
fmtp用于指定媒体数据格式。我们来解释下a=fmtp:111 minptime=10;useinbandfec=1
类型为111的数据,以10ms长的音频为一帧并且数据是经FEC编码的。
视频媒体信息
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
...
a=mid:1
...
a=rtpmap:96 VP8/90000
...
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
...
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
...
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
...
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
...
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
...
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
...
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:35 AV1X/90000
...
a=rtpmap:36 rtx/90000
a=fmtp:36 apt=35
a=rtpmap:124 H264/90000
...
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=124
a=rtpmap:123 H264/90000
...
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:118 rtx/90000
a=fmtp:118 apt=123
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
其中1行描述视频的媒体信息,第三行表示视频媒体的ID编号为1.
第5行a=rtpmap:96 VP8/90000
,payload值为96表示媒体数据使用的编码器是VP8,其时钟频率为90000。又因为其排在m=列表的第一位所以它是视频的默认编码器。
第7行a=rtpmap:97 rtx/90000
,payload值为97,rtx表示不再是编码器而是丢包重传,其要结合第8行代码a=fmtp:97 apt=96
一起看。apt的值为96表示96与97是关联在一起的。所以整体含义是:当webrtc使用媒体类型是96时如果出现丢包需要重传,重传数据包类型为97。
第50行a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
- level-asymmetry-allowed=1指明通信双方使用的H264Level是否要保持一致,0必须一致,1可以不一致。
- packetization-mode指明经H264编码后的视频数据如何打包:0单包、1非交错包、2交错包。三种打包模式中,模式0和模式1用于低延迟的实时通信领域。
- 模式0的含义是每个包就是一帧视频数据。
- 模式1是可以将视频帧拆分成多个顺序的RTP包发送,接收端收到数据包后再按顺序将其还原。
- profile-level-id由三部分组成,即profile_idc、profile_iop以及level_idc,每个组成占8位,因此可以推测出profile_idc=64、profile_iop=00、level-idc=1f
第53行a=rtpmap:114 red/90000
,red是一种在webrtc中使用的FEC(引入前向纠错)算法,用于防止丢包;
red编码流程,默认情况下webrtc会将VP8/H264等编码器编码后的数据再交由red模块编码,生成带一些冗余信息的数据包,这样当传输中某个包丢了,就可以通过其他包将其恢复回来,而不用重传丢失的包。
SSRC与CNAME
SSRC是媒体源的唯一标识,每一路媒体流都有一个唯一的SSRC标识它。
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
...
a=ssrc-group:FID 3004200836 146948263
//视频流的SSRC
a=ssrc:3004200836 cname:hO7txeyp3DC4HQ6j
a=ssrc:3004200836 msid:- 30c268ef-b3d0-42d2-ba35-0c8cb207505f
a=ssrc:3004200836 mslabel:-
a=ssrc:3004200836 label:30c268ef-b3d0-42d2-ba35-0c8cb207505f
//丢包重传的SSRC
a=ssrc:146948263 cname:hO7txeyp3DC4HQ6j
a=ssrc:146948263 msid:- 30c268ef-b3d0-42d2-ba35-0c8cb207505f
a=ssrc:146948263 mslabel:-
a=ssrc:146948263 label:30c268ef-b3d0-42d2-ba35-0c8cb207505f
从代码中看到由两个SSRC,第三行a=ssrc-group:FID 3004200836 146948263
,描述了SSRC(146948263)是SSRC(3004200836)的重传流。也就说3004200836是真正代表视频的SSRC,而146948263是视频流3004200836丢包时使用的SSRC,也就是为什么在同一个视频描述中有两个SSRC。
cnmae(canonical name)通常称为别名,可以用在很多地方,其中用的最多是在域名解析中,你想为某个域名起别名时就可以使用它。在两个SSRC中跟着同样的SSRC说明这两个SSRC属于同一个媒体流。
PlanB与UnifiedPlan
PlanB:只有两个媒体描述,即音频媒体描述(m=audio…)和视频媒体描述(m=video…)。如果要传输多路视频,则他们在视频媒体描述中需要通过SSRC来区分。
UnifiedPlan中可以有多个媒体描述,因此对于多路视频,将其拆成多个视频媒体描述即可。
//PlanB
m=audio...
a=ssrc:11223344
...
m=video ...
...
a=ssrc:22223333 cname:video1
...
a=ssrc:33334444 cname:video2
...
//UnifiedPlan
m=audio...
a=ssrc:11223344
...
m=video..
...
a=ssrc:22223333 cname:video1
...
m=video..
...
a=ssrc:33334444 cname:video2
...
PlanB 和 UnifiedPlan 其实就是 WebRTC 在多路媒体源(multi media source)场景下的两种不 同的 SDP 协商⽅式。如果引⼊ Stream 和 Track 的概念,那么⼀个 Stream 可能包含AudioTrack 和 VideoTrack,当有多路 Stream 时,就会有更多的 Track,如果每⼀个 Track 唯⼀对应⼀个⾃⼰的m描述,那么这就是 UnifiedPlan,如果每⼀个m=描述了多个Track(track id),那么这就是 Plan B。
网络描述
c=字段通常在webrtc中不适用该属性所以此处就不介绍。
a=candidate
Candidate 就是传输的候选⼈,客户端会⽣成多个 Candidate,⽐如有 host 、srflx 、prflx、 relay 类型,分有 UDP 和 TCP 的。
a=candidate:0 1 udp 2130706431 116.62.127.81 8000 typ host generation 0
a=rtcp-mux和a=group:BUNDLE
传输时,可以复用媒体通道,一种是音频和视频的复用,一种是RTCP和RTP的复用。
RTCP 和 RTP 复用,表示 Sender 使用一个传输通道(单一端口)发送 RTP 和 RTCP:
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
a=rtcp-mux
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
a=rtcp-mux
此时,Receiver 必须准备好在 RTP 端口上接收 RTCP 数据,并需要预留一些资源,比如 RTCP 带宽。
音频和视频复用时,最后只会用一个 Candidate 传输,比如客户端自己的 SDP Offer,和两个 relay 的Candidates:
a=group:BUNDLE 0 1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
a=mid:0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
a=mid:1
rtcp-mux 将 RTP 和 RTCP 复用到单一的端口进行传输,这简化了 NAT traversal,而 BUNDLE 又将多路媒体流复用到同一端口进行传输,这不仅使 candidate harvesting 等 ICE 相关的 SDP 属性变得简单,而且又进一步简化了 NAT traversal。
rtcp-mux 是与 RTC 传输相关的重要的 SDP 属性,关于它的 SDP 协商的原则如下:
- 如果 Offer 携带 rtcp-mux 属性,并且 Answer 方希望复用 RTP 和 RTCP 到单一端口,那么 Answer 必须也要携带该属性。
- 如果 Offer 没有携带 rtcp-mux 属性,那么 Answer 也一定不能携带 rtcp-mux 属性,而且 Answer 方禁止 RTP 和 RTCP 复用单一端口。
- rtcp-mux 的协商和使用必须是双向的。
举个例子。客户端去订阅服务器的流,客户端的 Offer 没有携带 rtcp-mux 属性,那么服务器会认为客户端不支持 rtcp-mux,也不会走 rtcp 复用的流程。相反,服务器会分别创建 RTP 和 RTCP 两个传输通道,只有当两个通道的 ICE 和 DTLS 都成功,才会认为本次订阅的传输通道建立成功,继而向客户端发流。
试想,如果因为你的疏忽导致 Offer 漏掉了 rtcp-mux 属性,那么你将永远等不到服务器 Ready 的那一天。所以,SDP 看似只是一些文本,很简单,但是只有在项目的实战中,多遇到几个坑,才能更深切的体会到 SDP 属性的含义以及这些属性是如何在 RTC 场景中去发挥作用的。
关于 rtcp-mux 更详细的协商细节,请参考RFC 8035。
关于 rtcp-mux 场景下如何通过头部字段区分 rtp 和 rtcp,请参考 RFC 5761。
媒体方向a=sendrecv
媒体流的方向有四种,分别是 sendonly、recvonly、sendrecv、inactive,它们既可以出现在会话级别描述中也可以出现在媒体描述中。
- sendonly 表示只发送数据,比如客户端推流到 SFU,那么会在自己的 Offer(or Answer) 中携带 senonly 属性
- revonly 表示只接收数据,比如客户端向 SFU 订阅流,那么会在自己的 Offer(or Answer) 中携带 recvonly 属性
- sendrecv 表示可以双向传输,比如客户端加入到视频会议中,既要发布自己的流又要订阅别人的流,那么就需要在自己的 Offer(or Answer) 中携带 sendrecv 属性
- inactive 表示禁止发送数据,比如在基于 RTP 的视频会议中,主持人暂时禁掉用户 A 的语音,那么用户 A 的关于音频的媒体级别描述应该携带 inactive 属性,表示不能再发送音频数据。
注意:senonly 和 recvonly 属性仅应用于媒体,不用于媒体控制相关的协议。比如在基于 RTP 的媒体会话中,即使是 recvonly 模式,也仍然要发送 RTCP 包,即使是 senonly 模式,也依然会接收并正常处理 RTCP 包。
媒体流方向的四个属性很重要,在组装 SDP 时要仔细校验,保证流方向的正确性。例如客户端去订阅服务器的流。如果此时客户端的 Offer 携带的属性并不是 recvonly 而是 sendonly,那么即使在信令层面的确是订阅的语义,但是由于某些服务器对 SDP 各属性的校验是十分全面和严格的(本该如此),这种场景下,服务器将不会发送媒体流到客户端,而且服务器回复的 Answer 可能根本不会携带 SSRC。
RTP扩展头a=extmap
a=extmap:<value>["/"<direction>] <URI> <extensionattributes>
extmap时extension map的缩写,即RTP Header扩展映射表。详情请参考RFC8285
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
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=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
...
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
...
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:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:11 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:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
URI是RTP扩展头对应值的规范说明,比如说transport-cc扩展头为3,扩展头的格式及含义都记录在http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01中
对于RTP而言,他的扩展头是以32位的整数倍增加的即每个扩展头最小是4个字节。
媒体类型 | 值 | URI |
---|---|---|
音频 | 1 | urn:ietf:params:rtp-hdrext:ssrc-audio-level |
2 | http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time | |
3 | http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 | |
4 | urn:ietf:params:rtp-hdrext:sdes:mid | |
5 | urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id | |
6 | urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id | |
视频 | 2 | http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time |
3 | http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 | |
4 | urn:ietf:params:rtp-hdrext:sdes:mid | |
5 | urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id | |
6 | urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id | |
7 | http://www.webrtc.org/experiments/rtp-hdrext/video-timing | |
8 | http://www.webrtc.org/experiments/rtp-hdrext/color-space | |
11 | http://www.webrtc.org/experiments/rtp-hdrext/video-content-type | |
12 | http://www.webrtc.org/experiments/rtp-hdrext/playout-delay | |
13 | urn:3gpp:video-orientation | |
14 | urn:ietf:params:rtp-hdrext:toffset |
安全描述
webRTC实现对音视频通信的防护有三个:应用级别防护、信令级别防护、数据级防护。
- 应用级防护:用户使用音视频产品时一般都要先注册然后再使用用户名密码的方式登录到应用系统。
- 信令级防护:当用户通过第一级防护后,就可以获得webrtc信令服务器的地址,并通过信令服务器进行媒体协商。媒体协商后,通信双方便可以从彼此的SDP中获取对方的用户名/密码,即ice-ufrag和ice-pwd信息。这两个信息的作用是验证用户的合法性。通信双方彼此发送STUN Binding请求(带着ice-ufrag和ice-pwd)给对方,当对方收到请求后会验证请求ice-ufrag和ice-pwd是否与自己的SDP中的一致,如果一致则说明该用户时合法的用户否则说明它是非法用户。
- 媒体数据加密:对于非法用户即使突破第二级防护拿到媒体数据也无法讲这些数据还原成音视频,因为这些数据是加密过的。在信令完成后通信双方还会使用DTLS协议彼此交换证书,证书中保存的最重要的就是公钥。例如ClientA和ClientB发送媒体数据,ClientA需要用ClientB的公钥对数据加密,加密后的数据再打包成SRTP发送给ClientB,Client再使用自己的私钥将数据解密。
a=ice-ufrag:wU6B
a=ice-pwd:PQcUgUs0wp6k8lBAokxMBf7K
...
a=fingerprint:sha-256 AA:72:3F:54:CD:CF:B9:EF:11:2F:41:81:2D:A4:32:B3:EF:EF:88:C8:F1:4A:F9:C1:AF:24:6A:70:BC:10:65:F9
a=setup:actpass
第1行中a=ice-ufrag:wU6B
表示用户名,第2行a=ice-pwd:PQcUgUs0wp6k8lBAokxMBf7K
表示密码。webrtc相互通信后需要使用这两个值进行用户有效性验证。
第4行a=fingerprint
用于验证加密证书的有效性。当通信双方通过DTLS协议交换证书时,如何保障证书在网络交换的过程中没有被篡改?
- 各端会给各自的证书生成一个指纹。
- 将指纹放到SDP中通过信令交换给对方。
- DTLS协议交换证书。
- 将拿到的证书重新生成指纹。
- 将生成的指纹与SDP中的指纹进行比较,如果两者一致,则说明证书在传输过程中没有被篡改可以使用,否则说明证书被篡改此时连接创建失败。
第5行a=setup:actpass
决定DTLS协议时通信双方的角色。具体如下:
- active:终端的角色为客户端。
- passive:终端的角色为服务端。
- actpass:终端既可以是客户端也可以是服务端。
最终的角色由另一端角色确定,一般第一个加入房间的终端默认为actpass,后来加入的终端为active。
服务质量a=rtcp-fb
媒体描述中服务质量是由a=rtcp-fb
描述的。rtcp-fb有两层含义
- RTCP消息中专门反馈信息的消息
- 设置webrtc支持哪些rtcp feedback消息
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
a=rtcp-fb:111 transport-cc
...
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
...
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
...
在WebRTC中有两种拥塞控制算法:
- Goog-REMB
- Transport-CC
第3行代码的含义是,在使用payload类型为111的编解码器时,支持Transport-CC类型的rtcp feedback报文,同时也说明webrtc在使用Opus编解码器时开启了Transport-CC拥塞控制算法。
第7-8行说明webrtc使用VP8编解码器时,既支持Goog-REMB的RTCP报文,也支持Transport-CC的RTCP报文。
第9行ccm(codec control message)和fir(full intra refresh)指明webrtc支持RTCP的FIR指令(申请关键帧)
第10行指明webrtc支持nack报文
第11行说明支持nack报文的同时将支持PLI报文。
SDP示例
offer example
//版本信息,一般都是0
v=0
//会话的创建者
o=- 1954504395161900476 2 IN IP4 127.0.0.1
//会话名
s=-
//会话时长
t=0 0
//音视频采取多路复用的方式,通过同一个通道传输,减少对ICE资源的消耗
a=group:BUNDLE 0 1
//Chrome自从M71版本就开始支持SDP协议属性extmap-allow-mixed
//但是如果提供了extmap-allow-mixed,M71之前版本Chrome的SDP协商将会失败。
//从Chrome M89版本开始,extmap-allow-mixed 将被默认提供。
a=extmap-allow-mixed
//WebRTC Media Stream
a=msid-semantic: WMS
// 音频描述
// 端口9忽略,端口为0表示不传输音频
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
//网络描述,在webrtc中不使用该属性
c=IN IP4 0.0.0.0
//在webrtc中不使用该属性
a=rtcp:9 IN IP4 0.0.0.0
// 用于ICE有效用户的验证,ufrag表示用户名
a=ice-ufrag:wU6B
// 用于ICE有效用户的验证,pwd表示密码
a=ice-pwd:PQcUgUs0wp6k8lBAokxMBf7K
//收集candidate的方式
a=ice-options:trickle
//指纹,用于验证DTLS
a=fingerprint:sha-256 AA:72:3F:54:CD:CF:B9:EF:11:2F:41:81:2D:A4:32:B3:EF:EF:88:C8:F1:4A:F9:C1:AF:24:6A:70:BC:10:65:F9
//DTLS角色
a=setup:actpass
//BUNDLE,0表示音频
a=mid:0
//音频传输时RTP支持的扩展头,参考RFC6464
//发送端是否支持音频level扩展
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
//NTP时间扩展头
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
//transport-CC的扩展头
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
//与RTCP中的SDES相关的扩展头
//通过RTCP的SDES传输mid
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
//通过RTCP的SDES传输rtp-stream-id
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
//通过RTCP的SDES传输重传时的rtp-stream-id
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
//音频数据的传输防线,仅发送
a=sendonly
//记录音频与媒体流的关系
a=msid:- 6e7a4024-a8fc-4ae4-a48d-47c825a94b07
//RTCP与RTP复用传输通道
a=rtcp-mux
//payload=111代表音频编码器opus,采样率48000,双通道
a=rtpmap:111 opus/48000/2
//使用opus时,支持RTCP中的transport-cc反馈报文
a=rtcp-fb:111 transport-cc
//使用opus时,每个视频帧的最小间隔为10ms,使用带内频率
a=fmtp:111 minptime=10;useinbandfec=1
//payload=103代表音频解码器ISAC,采样率16000
a=rtpmap:103 ISAC/16000
//payload=104代表音频解码器ISAC,采样率32000
a=rtpmap:104 ISAC/32000
//payload=9代表音频解码器G722,采样率8000
a=rtpmap:9 G722/8000
//payload=0代表未压缩音频数据PCMU,采样率8000
a=rtpmap:0 PCMU/8000
//payload=8代表未压缩音频数据PCMA,采样率8000
a=rtpmap:8 PCMA/8000
//payload=106代表舒适噪声(Comfort Noise)CN,采样率32000
a=rtpmap:106 CN/32000
//payload=105代表舒适噪声CN,采样率16000
a=rtpmap:105 CN/16000
//payload=13代表舒适噪声CN,采样率8000
a=rtpmap:13 CN/8000
//payload=110 SIP DTMF 电话按键,采样率48000
a=rtpmap:110 telephone-event/48000
//payload=112 SIP DTMF 电话按键,采样率32000
a=rtpmap:112 telephone-event/32000
//payload=113 SIP DTMF 电话按键,采样率16000
a=rtpmap:113 telephone-event/16000
//payload=126 SIP DTMF 电话按键,采样率8000
a=rtpmap:126 telephone-event/8000
//原3120257347的别名为hO7txeyp3DC4HQ6j
a=ssrc:3120257347 cname:hO7txeyp3DC4HQ6j
//记录源3120257347与音频轨和媒体流的关系
a=ssrc:3120257347 msid:- 6e7a4024-a8fc-4ae4-a48d-47c825a94b07
//记录源3120257347属于那个媒体流
a=ssrc:3120257347 mslabel:-
//记录源3120257347属于那个媒体流
a=ssrc:3120257347 label:6e7a4024-a8fc-4ae4-a48d-47c825a94b07
//视频媒体描述
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116
//webrtc不使用该属性
c=IN IP4 0.0.0.0
//webrtc不使用该属性
a=rtcp:9 IN IP4 0.0.0.0
//用户
a=ice-ufrag:wU6B
//密码
a=ice-pwd:PQcUgUs0wp6k8lBAokxMBf7K
//设置收集candidate的方式
a=ice-options:trickle
//证书指纹,用于验证DTLS证书有效性
a=fingerprint:sha-256 AA:72:3F:54:CD:CF:B9:EF:11:2F:41:81:2D:A4:32:B3:EF:EF:88:C8:F1:4A:F9:C1:AF:24:6A:70:BC:10:65:F9
//指定DTLS角色
a=setup:actpass
//id 1
a=mid:1
//视频传输时RTP支持的扩展头,toffset(TransportTime Offset)
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
//RTP包中的timestamp与实际发送时的偏差
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
//视频旋转角度的扩展头
a=extmap:13 urn:3gpp:video-orientation
//Transport-CC扩展头
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
//发送端控制接收端渲染视频的延时时间
a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
//指定视频内容,有两个值:未指定和屏幕共享
a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
//该扩展在每个视频帧最后一个包中出现
//1. 编码开始
//2. 编码打包完成时间
//3. 打包完成时间
//4. 离开pacer的最后一个包的时间
//5. 预留时间1
//6. 预留时间2
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
//携带mid的扩展头
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
//携带rtp-stream-id的扩展头
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
//重传时携带的rtp-stream-id的扩展头
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
//视频数据传输方向,仅发送
a=sendonly
//视频流的id
a=msid:- 30c268ef-b3d0-42d2-ba35-0c8cb207505f
//rtcp与rtp复用端口
a=rtcp-mux
//减少RTCP尺寸
a=rtcp-rsize
//payload=96代表视频编解码VP8,采样率90000
a=rtpmap:96 VP8/90000
//payload=96支持goob-remb拥塞控制算法
a=rtcp-fb:96 goog-remb
//payload=96支持transport-cc拥塞控制算法
a=rtcp-fb:96 transport-cc
//payload=96支持ccm fir
a=rtcp-fb:96 ccm fir
//payload=96支持nack反馈
a=rtcp-fb:96 nack
//payload=96支持pli反馈
a=rtcp-fb:96 nack pli
//payload=97代表重传数据,采样率为90000
a=rtpmap:97 rtx/90000
//payload97与96时绑定关系,97是96的重传数据
a=fmtp:97 apt=96
//payload=98代表视频解码器VP9,采样率为90000
a=rtpmap:98 VP9/90000
//payload=98支持goog-remb
a=rtcp-fb:98 goog-remb
//payload=98支持transport-cc
a=rtcp-fb:98 transport-cc
//payload=98支持fir反馈
a=rtcp-fb:98 ccm fir
//payload=98支持nack反馈
a=rtcp-fb:98 nack
//payload=98支持pli反馈
a=rtcp-fb:98 nack pli
//使用VP9时,视频帧的profile id为0
//VP9一共有4种profile 1,2,3,4
//0表示支持8bit位深和yuv4:2:0格式
a=fmtp:98 profile-id=0
//payload=99代表重传数据,采样率90000
a=rtpmap:99 rtx/90000
//payload=99与98是绑定关系,99是98的重传数据
a=fmtp:99 apt=98
//payload=100表示视频解码器VP9,采样率90000
a=rtpmap:100 VP9/90000
//payload=100支持goog-remb
a=rtcp-fb:100 goog-remb
//payload=100支持transport-cc
a=rtcp-fb:100 transport-cc
//payload=100支持ccm fir
a=rtcp-fb:100 ccm fir
//payload=100支持nack
a=rtcp-fb:100 nack
//payload=100支持pli
a=rtcp-fb:100 nack pli
//使用VP9时,视频帧的profile id为2
//VP9一共有4种profile 1,2,3,4
//2表示支持10bit、12bit位深和yuv4:2:0格式
a=fmtp:100 profile-id=2
//payload=101是重传数据,采样率90000
a=rtpmap:101 rtx/90000
//payload=101与100绑定,101是100的重传数据
a=fmtp:101 apt=100
//payload=102表示视频编码器是H264,采样率90000
a=rtpmap:102 H264/90000
//payload=102支持goog-remb
a=rtcp-fb:102 goog-remb
//payload=102支持transport-cc
a=rtcp-fb:102 transport-cc
//payload=102支持fir反馈
a=rtcp-fb:102 ccm fir
//payload=102支持nack反馈
a=rtcp-fb:102 nack
//payload=102支持pli反馈
a=rtcp-fb:102 nack pli
//payload=102双方使用的h264级别可以不一致,编码后视频数据可以将视频拆分成多个RTP包发送
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
//payload=121是重传数据,采样率90000
a=rtpmap:121 rtx/90000
//payload=121与102绑定,121是102的重传数据
a=fmtp:121 apt=102
//payload=127表示视频编码器是H264,采样率90000
a=rtpmap:127 H264/90000
//payload=127支持goog-remb
a=rtcp-fb:127 goog-remb
//payload=127支持transport-cc
a=rtcp-fb:127 transport-cc
//payload=127支持fir反馈
a=rtcp-fb:127 ccm fir
//payload=127支持nack反馈
a=rtcp-fb:127 nack
//payload=127支持pli反馈
a=rtcp-fb:127 nack pli
//payload=127双方使用的h264级别可以不一致,每个包就是一帧视频数据
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
//payload=120是重传数据,采样率90000
a=rtpmap:120 rtx/90000
//payload=120与127绑定,120是127的重传数据
a=fmtp:120 apt=127
//payload=125表示视频编码器是H264,采样率90000
a=rtpmap:125 H264/90000
//payload=125支持goog-remb
a=rtcp-fb:125 goog-remb
//payload=125支持transport-cc
a=rtcp-fb:125 transport-cc
//payload=125支持fir反馈
a=rtcp-fb:125 ccm fir
//payload=125支持nack反馈
a=rtcp-fb:125 nack
//payload=125支持pli反馈
a=rtcp-fb:125 nack pli
//payload=125双方使用的h264级别可以不一致,编码后视频数据可以将视频拆分成多个RTP包发送
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
//payload=107是重传数据,采样率90000
a=rtpmap:107 rtx/90000
//payload=125与107绑定,107是125的重传数据
a=fmtp:107 apt=125
//payload=108表示视频编码器是H264,采样率位90000
a=rtpmap:108 H264/90000
//payload=108支持goog-remb
a=rtcp-fb:108 goog-remb
//payload=108支持transport-cc
a=rtcp-fb:108 transport-cc
//payload=108支持fir反馈
a=rtcp-fb:108 ccm fir
//payload=108支持nack反馈
a=rtcp-fb:108 nack
//payload=108支持pli反馈
a=rtcp-fb:108 nack pli
//payload=108双方使用的h264级别可以不一致,每个包就是一阵视频数据
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
//payload=109是重传数据,采样率90000
a=rtpmap:109 rtx/90000
//payload=108与109绑定,109是108的重传数据
a=fmtp:109 apt=108
//payload=35表示视频编码器是AV1X,采样率位90000
a=rtpmap:35 AV1X/90000
//payload=35支持goog-remb
a=rtcp-fb:35 goog-remb
//payload=35支持transport-cc
a=rtcp-fb:35 transport-cc
//payload=35支持fir反馈
a=rtcp-fb:35 ccm fir
//payload=35支持nack反馈
a=rtcp-fb:35 nack
//payload=35支持pli反馈
a=rtcp-fb:35 nack pli
//payload=36是重传数据,采样率90000
a=rtpmap:36 rtx/90000
//payload=36与35绑定,36是35的重传数据
a=fmtp:36 apt=35
//payload=124表示视频编码器是H264,采样率位90000
a=rtpmap:124 H264/90000
//payload=124支持goog-remb
a=rtcp-fb:124 goog-remb
//payload=124支持transport-cc
a=rtcp-fb:124 transport-cc
//payload=124支持fir反馈
a=rtcp-fb:124 ccm fir
//payload=124支持nack反馈
a=rtcp-fb:124 nack
//payload=124支持pli反馈
a=rtcp-fb:124 nack pli
//payload=124双方使用的h264级别可以不一致,编码后视频数据可以将视频拆分成多个RTP包发送
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
//payload=119是重传数据,采样率90000
a=rtpmap:119 rtx/90000
//payload=124与119绑定,119是124的重传数据
a=fmtp:119 apt=124
//payload=123表示视频编码器是H264,采样率位90000
a=rtpmap:123 H264/90000
//payload=123支持goog-remb
a=rtcp-fb:123 goog-remb
//payload=123支持transport-cc
a=rtcp-fb:123 transport-cc
//payload=123支持fir反馈
a=rtcp-fb:123 ccm fir
//payload=123支持nack反馈
a=rtcp-fb:123 nack
//payload=123支持pli反馈
a=rtcp-fb:123 nack pli
//payload=123双方使用的h264级别可以不一致,编码后视频数据可以将视频拆分成多个RTP包发送
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
//payload=118是重传数据,采样率90000
a=rtpmap:118 rtx/90000
//payload=118与123绑定,118是123的重传数据
a=fmtp:118 apt=123
//payload=114引入FEC算法防止丢包
a=rtpmap:114 red/90000
//payload=115是重传数据,采样率9000
a=rtpmap:115 rtx/90000
//payload=115与114绑定,115是1114的重传数据
a=fmtp:115 apt=114
//payload=116,使用ulp fec技术,采样率90000
a=rtpmap:116 ulpfec/90000
//指明几个源的关系
//格式:a=ssrc-group:<semantics> <ssrc-id>...参考RFC5576
//FID表示这几个源都是数据流
//其中3004200836是正常流
//146948263是重传流
a=ssrc-group:FID 3004200836 146948263
a=ssrc:3004200836 cname:hO7txeyp3DC4HQ6j
a=ssrc:3004200836 msid:- 30c268ef-b3d0-42d2-ba35-0c8cb207505f
a=ssrc:3004200836 mslabel:-
a=ssrc:3004200836 label:30c268ef-b3d0-42d2-ba35-0c8cb207505f
a=ssrc:146948263 cname:hO7txeyp3DC4HQ6j
a=ssrc:146948263 msid:- 30c268ef-b3d0-42d2-ba35-0c8cb207505f
a=ssrc:146948263 mslabel:-
a=ssrc:146948263 label:30c268ef-b3d0-42d2-ba35-0c8cb207505f
answer example
//版本信息
v=0
//会话的创建者
o=SRS/4.0.198(Leo) 353875216 2 IN IP4 0.0.0.0
//会话名
s=SRSPublishSession
//会话时长
t=0 0
//客户端不收集候选者对象
a=ice-lite
//音视频传输采取多路复用方式,通过一个通道传输,减少对ICE的消耗
a=group:BUNDLE 0 1
//媒体流ID
a=msid-semantic: WMS 61a5cf9460e3d815f0c9f5eb/22ed5d98105
//音频媒体描述
m=audio 9 UDP/TLS/RTP/SAVPF 111
//webrtc忽略该属性
c=IN IP4 0.0.0.0
//用户名
a=ice-ufrag:40175i90
//密码
a=ice-pwd:d8c03p024926j298754m301831923641
//指纹
a=fingerprint:sha-256 E8:D1:F9:1D:E3:14:15:A2:26:4D:1F:AB:73:74:06:E9:73:D3:98:2B:F6:AA:7B:7B:94:84:2B:C6:3F:17:48:ED
//DTLS角色
a=setup:passive
//BUNDLE,0表示音频
a=mid:0
//transport-cc拓展头
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
//仅接受
a=recvonly
//RTCP与RTP复用传输通道
a=rtcp-mux
//减小RTCP的尺寸
a=rtcp-rsize
//payload=111代表音频编码器opus,采样率48000,双通道
a=rtpmap:111 opus/48000/2
//使用opus时使用transport-cc算法
a=rtcp-fb:111 transport-cc
//使用opus时,每个视频帧的最小间隔10ms,使用带内频率
a=fmtp:111 minptime=10;useinbandfec=1
//音频传输的候选者信息
a=candidate:0 1 udp 2130706431 116.62.127.81 8000 typ host generation 0
//视频媒体描述
m=video 9 UDP/TLS/RTP/SAVPF 125 114
//webrtc忽略该属性
c=IN IP4 0.0.0.0
//用户名
a=ice-ufrag:40175i90
//密码
a=ice-pwd:d8c03p024926j298754m301831923641
//证书指纹
a=fingerprint:sha-256 E8:D1:F9:1D:E3:14:15:A2:26:4D:1F:AB:73:74:06:E9:73:D3:98:2B:F6:AA:7B:7B:94:84:2B:C6:3F:17:48:ED
//DTLS角色
a=setup:passive
//BUNDLE,1表示视频
a=mid:1
//transport-cc扩展头
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
//仅接受
a=recvonly
//RTCP和RTP复用
a=rtcp-mux
//减小RTCP的尺寸
a=rtcp-rsize
//payload=125,视频编码器使用H264,采样率90000
a=rtpmap:125 H264/90000
//H264支持transport-cc
a=rtcp-fb:125 transport-cc
//H264支持nack反馈
a=rtcp-fb:125 nack
//H264支持pli反馈
a=rtcp-fb:125 nack pli
//H264双方使用的h264级别可以不一致,编码后视频数据可以将视频拆分成多个RTP包发送
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
//payload=114视频采用red fec技术,采样率为90000
a=rtpmap:114 red/90000
//音频传输的候选者信息
a=candidate:0 1 udp 2130706431 116.62.127.81 8000 typ host generation 0
与SIP对比
RTC 场景与 SIP 场景下的 SDP 描述的不同表现在传输、媒体、信令三个层面。
传输
- 建连流程。RTC 场景下的音视频媒体流建连流程一般是 ICE + DTLS,而 SIP 场景下没有这套建连流程,所以也没有 ICE/DTLS 相关的 SDP 属性,比如 ufrag、pwd、setup、fingerprint 等。
- 端口复用。RTC 场景下一般都是音视频流以及 RTP/RTCP 复用单一端口,通过 SSRC 区分每一路流,通过数据包的头部字段值来区分 RTP/RTCP,而 SIP 场景下不会复用端口,因此没有 rtcp-mux 属性,也没有 grouping 相关的属性,比如 BUNDLE,且音视频的 RTP 和 RTCP 都是独立端口进行传输,共有四个,所以天然可以使用端口来区分流以及 RTP/RTCP,因此也没有 SSRC 属性。
- 链路探测。RTC 场景下一般通过 ICE 的 STUN 探测环节来发现对端经过 NAT 映射之后的出口地址,称为 srflx,而 SIP 场景下需要自己实现对端地址发现的功能,以获取到 SIP 设备经过 NAT 映射之后的出口地址。
- 地址信息。RTC 场景下通过 SDP 的 candidate 交换对端地址信息,SIP 场景下通过 C line 的 ip 以及 M line 的端口来交换对端地址信息。
// RTC 场景a=candidate:1 1 udp 2013266431 30.27.136.138 14306 typ host
// SIP 场景c=IN IP4 30.41.5.131m=audio 2352 RTP/AVP 107 108 114 104 105 9 18 8 0 101 123m=video 2374 RTP/AVP 97 126 96 34 123
媒体
- 屏幕共享。SIP 场景下通过 BFCP 协议来进行屏幕共享的协商,通过 a=content 属性来区分主流(main)和共享流 (slides),而 RTC 场景下通过外部/业务信令来进行屏幕共享的协商,主流和共享流的 SDP 描述一致,不会区分。
- Media Codec。目前,RTC 场景下的音视频编码普遍是 Opus + H.264/VP8,SIP 场景下,对于音频编码,有很多 SIP 设备并不支持 Opus,而采用比较古老的音频编码,比如 G722、PCMA、PCMU,对于视频编码,普遍支持 H.264,一般不支持 VP8。
信令
- SDP 交换。都是 Offer/Answer 模型,RTC 场景下主要通过 HTTP/TCP 协议交换 SDP,一般是在 HTTP body 中携带 SDP 信息。SIP 场景下可以通过 UDP/TCP/TLS 协议交换 SDP,在 INVITE 和 200 OK 中携带 SDP 信息。
参考文档
1998,RFC2327,SDP: Session Description Protocol
2006,RFC4566, SDP: Session Description Protocol
WebRTC SDP 详解和剖析
WebRtc-internals
Identifiers for WebRTC’s Statistics API
WebRTC音视频实时互动技术:原理、实战与源码刨析
[RFC6464]实时传输协议 (RTP) 标头扩展
webrtc推荐教程:免费FFMPEG/srs/WEBRTC课程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/137647.html