简介

Simulcast 可以翻译成联播,不是新闻联播,而是多个媒体流的传播,如图所示

注: SFU 即 Selective Forward Unit, 媒体转发服务器

刚开始,发送方 sender 发送一路 1080p 的视频流给服务器 SFU, SFU 再转发给多个接收方 receiver1, receiver2 和 receiver3。

一会儿,receiver 2 和 receiver 3 通过 RTCP 反馈带宽不够,丢包率和延迟都比较大,也就是说

  • 上行带宽对于 sender 是足够的,
  • 下行带宽对于 receiver 1 也是没问题的
  • 下行带宽对于 receiver 2 和 receiver 3 都有问题

这样联播的好处就来了,SFU 可以根据带宽评估的结果让 Sender 同时发送 1080p, 360p 和 180p 三路流,并分别转发给 receiver1, receiver 2 和 receiver 3。

为了让浏览器和服务器都支持 simulcast ,我们需要在 SDP 和 RTP 包格式上做出些扩展,目的是让 SFU 和接收端知道哪几路流是隶属于一个媒体源的,而且它们的参数是多少, 例如

  • 带宽bitrate
  • 分辨率 resolution
  • 帧率 frameRate

RFC8853 对此有详细阐述

术语

  • Simulcast stream: 一组同时传输的媒体编码流和可选的依赖的媒体流其中中的一个媒体流,它们都共享一个共同的媒体源 (media source,例如一个摄像头) ,如 RFC7656 中所定义。
    例如,单个媒体源的高清图像和缩略图像的视频流由联播各自作为单独的 RTP 流同时发送。

  • Simulcast format: simulcast stream 的不同格式与非 simulcast stream 的 SDP 中的替代 RTP 有效payload 类型具有相同的目的:允许指定 RTP 流中使用多种不同的可替代的媒体格式。

对于在 SDP Offer/Answer 中的 m-line 里有多个 RTP payload 的情形,任何一种协商的替代格式都可以在给定的时间点在单个 RTP 流中使用,但不超过一种(基于 RTP 时间戳 ), 换句话说,即同一时刻的单个 RTP stream 中不会有多种 Payload.使用的格式可以从一个 RTP 数据包动态更改为另一个。

SDP 对于 Simulcast 的支持

在 SDP 扩展中,主要有两种方法来告知服务器及接收方哪些流是 simulcast stream

  1. SSRC Group: 将 simulcast streams 放置在一个个组中
  2. RtpStreamId: 为 simulcast streams 加一个标识,并在 RTP 包中携带。

1) SSRC Group

早期为了支持 RTX(RTP Retransmission Payload Format) , 参见RFC4588 , SDP 支持用 SSRC group 的方式来声明媒体流和重传流之间的关系

如下例所示,这里有两路流,一路流是 6596, 另一路流是9814,它们其实还是单流 9814 是用来重传丢失包的 retransmission的, 用一个 FID ssrc-group 将它们关联起来

a=ssrc:659652645 cname:Taj3/ieCnLbsUFoH
a=ssrc:659652645 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:659652645 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:659652645 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 cname:Taj3/ieCnLbsUFoH
a=ssrc:98148385 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:98148385 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc-group:FID 659652645 98148385

为支持多流(multi stream)和联播(simulcast) , 又引入了 SIM ssrc group, 例如:

a=ssrc:659652645 cname:Taj3/ieCnLbsUFoH
a=ssrc:659652645 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:659652645 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:659652645 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 cname:Taj3/ieCnLbsUFoH
a=ssrc:98148385 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:98148385 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:98148385 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1982135572 cname:Taj3/ieCnLbsUFoH
a=ssrc:1982135572 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1982135572 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:1982135572 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:2523084908 cname:Taj3/ieCnLbsUFoH
a=ssrc:2523084908 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:2523084908 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:2523084908 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:3604909222 cname:Taj3/ieCnLbsUFoH
a=ssrc:3604909222 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:3604909222 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:3604909222 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1893605472 cname:Taj3/ieCnLbsUFoH
a=ssrc:1893605472 msid:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk 028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc:1893605472 mslabel:i1zOaprU7rZzMDaOXFdqwkq7Q6wP6f3cgUgk
a=ssrc:1893605472 label:028ab73b-cdd0-4b61-a282-ea0ed0c6a9bb
a=ssrc-group:SIM 659652645 1982135572 3604909222
a=ssrc-group:FID 659652645 98148385
a=ssrc-group:FID 1982135572 2523084908
a=ssrc-group:FID 3604909222 1893605472

可以从上面的 SDP 得知, 有三路流, SSRC 分别为 659652645, 1982135572, 3604909222, 它们为一个联播组 simulcast group,表示为时间上(temporal layers)或空间上(spatial layers)不同的质量的媒体流(frameRate 或 resolution 不同),每一路流又有各自的重传流,一共有 3 * 2 = 6 个 ssrc, Chrome and Safari 按照质量的递增顺序 i (low, medium and high quality) 来排列 SSRCs

这种方法显然不太好,一是比较繁琐,二是没有显式的指明每路流的参数https://w3c.github.io/webrtc-pc/ 标准中 5.4.1 Simulcast functionality 提到

Simulcast 经常用于向 SFU 发送多个编码,然后 SFU 会将其中一个或几个 Simulcast 流转发给不同的最终用户。 因此,SFU 需要在不同的编码之间分配合理的带宽,以使所有 Simulcast 流都可以使用各自拥有的带宽; 例如,如果两个Simulcast 流 具有相同的 maxBitrate 参数,人们会期望在两个流上看到相似的比特率。 如果带宽不允许所有simulcast 流以可用的形式发送,SFU 应该停止发送一些 simulcast 流,或者要求发送改变发送参数。

发送端会发送一个或多个 simulcast 流, 或者在不同参数(码率,分辨率,帧率等) 的流之间切换,这些流有独自的 SSRC 和 SequenceNumber 空间,这对 SFU 和接收端都有一定的要求

2) RtpStreamId

这种方法也是 RFC8853 中的推荐做法,为 RTP 包增加一个扩展头 RtpStreamId, 在 SDP 中为不同的 simulcast 流声明一个 rid 标识。 例如, SDP Offer 如下

  • SDP Offer
m=video 49300 RTP/AVP 97 98 99
   a=rtpmap:97 H264/90000
   a=rtpmap:98 H264/90000
   a=rtpmap:99 VP8/90000
   a=fmtp:97 profile-level-id=42c01f;max-fs=3600;max-mbps=108000
   a=fmtp:98 profile-level-id=42c00b;max-fs=240;max-mbps=3600
   a=fmtp:99 max-fs=240; max-fr=30
   a=rid:1 send pt=97;max-width=1280;max-height=720
   a=rid:2 send pt=98;max-width=320;max-height=180
   a=rid:3 send pt=99;max-width=320;max-height=180
   a=rid:4 recv pt=97
   a=simulcast:send 1;2,3 recv 4
   a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id

提供者能够发送两个联播 RTP 流:一个高达 720p 分辨率的 H.264 编码流,以及一个编码为 H.264 或 VP8 的附加流,最大分辨率为 320x180 像素。 提供者可以接收一个最大 720p 分辨率的 H.264 流

  • SDP Answer
m=video 49674 RTP/AVP 97 98
   a=rtpmap:97 H264/90000
   a=rtpmap:98 H264/90000
   a=fmtp:97 profile-level-id=42c01f;max-fs=3600;max-mbps=108000
   a=fmtp:98 profile-level-id=42c00b;max-fs=240;max-mbps=3600
   a=rid:1 recv pt=97;max-width=1280;max-height=720
   a=rid:2 recv pt=98;max-width=320;max-height=180
   a=rid:4 send pt=97
   a=simulcast:recv 1;2 send 4
   a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id

在这个 SDP 应答中,“recv”部分表明它想要接收两个H264 编码的 Simulcast 流。 并已经删除了它不支持的 VP8 编码的 Simulcast 流(rid=3)。 “发送”部分向 offer 方确认它将根据 rid=4 来发送一个媒体流

联播和冗余

为了提高抗丢包的能力,在发送 Simulcast 流时,通常我们会发送一些冗余的 RTP 包,当一些包丢失了,我们可以在接收端通过冗余包来恢复,常用的方法有 FEC 和 RTX。 为了让大家知道冗余包所在的媒体流,我们基于 RtpStreamId(rid) 的方案可以再引入一年 RepeairedRtpStreamId - rrid

例如,对于单流的情况,一路媒体流,一路媒体冗余流,只需要通过 payload type 的不同就可以区分出来了

v=0
   o=mascha 2980675221 2980675778 IN IP4 host.example.net
   c=IN IP4 192.0.2.0
   a=group:FID 1 2
   a=group:FID 3 4
   m=audio 49170 RTP/AVPF 96
   a=rtpmap:96 AMR/8000
   a=fmtp:96 octet-align=1
   a=rtcp-fb:96 nack
   a=mid:1
   m=audio 49172 RTP/AVPF 97
   a=rtpmap:97 rtx/8000
   a=fmtp:97 apt=96;rtx-time=3000
   a=mid:2
   m=video 49174 RTP/AVPF 98
   a=rtpmap:98 MP4V-ES/90000
   a=rtcp-fb:98 nack
   a=fmtp:98 profile-level-id=8;config=01010000012000884006682C209\
   0A21F
   a=mid:3
   m=video 49176 RTP/AVPF 99
   a=rtpmap:99 rtx/90000
   a=fmtp:99 apt=98;rtx-time=3000
   a=mid:4

上例中通过 a=fmtp:99 apt=98;rtx-time=3000 我们就知道这路流是 RTX 流,它的 payload type 是99, 而它所要修复的流的 Payload type 是98。

但是对于多路 Simulcast 流,仅仅用 payload type 来区分就不够了,假设用三路媒体流, 它们的分辨率不同,但是 payload type 都一样,不过 rid 是不一样的,同时,每一路流还有相应的冗余流,这就有点麻烦了。这时候,我们可以引入 rrid(repaired-rtp-stream-id)

RFC 8853 中举了一个例子,使用了音视频的 Simulcast 和冗余(FEC/RTX)格式。

  • Audio SDP Offer 使用了编解码器和比特率限制,结合了冗余音频数据 RED [RFC2198] 以增强丢包弹性。

音频作为两个 Simulcast 流发送。第一个Simulcast 流使用 Opus 编码,限制为 64 kbps (rid=1),第二个Simulcast 流 (rid-id=2) 被编码与 G.711 或 G.711 结合线性预测编码 (LPC) 用于冗余和显式舒适噪声 (CN)。两个 Simulcast 流都包括了 Telephony Event 功能( 用于 DTMF 传输)。

在这个例子中, 单独的 LPC 不作为第二个可能的有效载荷类型提供 Simulcast 流的 RID,其动机可能是其不提供足够好的音频质量。

  • Video SDP Offer 应用了分辨率和比特率的限制,并结合了前向纠错 (FEC) RFC8627 和 RTP 重传 RFC4588

视频源可作为两个Simulcast流发送,均具有两种可选Simulcast格式。 冗余和修复以 Flexible FEC 和 RTP 重传的形式提供。 Flexible FEC 不绑定到任何特定的 RTP 流,因此能够在作为该媒体描述的一部分发送的所有 RTP 流中使用。

o=fred 238947129 823479223 IN IP6 2001:db8::c000:27d
   s=Offer from Simulcast-Enabled Client using Redundancy
   c=IN IP6 2001:db8::c000:27d
   t=0 0
   a=group:BUNDLE foo bar
   m=audio 49200 RTP/AVP 97 98 99 100 101 102
   a=mid:foo
   a=rtpmap:97 G711/8000
   a=rtpmap:98 LPC/8000
   a=rtpmap:99 OPUS/48000/1
   a=rtpmap:100 RED/8000/1
   a=rtpmap:101 CN/8000
   a=rtpmap:102 telephone-event/8000
   a=fmtp:99 useinbandfec=1;usedtx=0
   a=fmtp:100 97/98
   a=fmtp:102 0-15
   a=ptime:20
   a=maxptime:40
   a=rid:1 send pt=99,102;max-br=64000
   a=rid:2 send pt=100,97,101,102
   a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
   a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
   a=simulcast:send 1;2
   m=video 49600 RTP/AVPF 103 104 105 106 107
   a=mid:bar
   a=rtpmap:103 H264/90000
   a=rtpmap:104 VP8/90000
   a=rtpmap:105 rtx/90000
   a=rtpmap:106 rtx/90000
   a=rtpmap:107 flexfec/90000
   a=fmtp:103 profile-level-id=42c00d;max-fs=3600;max-mbps=108000
   a=fmtp:104 max-fs=3600; max-fr=30
   a=fmtp:105 apt=103;rtx-time=200
   a=fmtp:106 apt=104;rtx-time=200
   a=fmtp:107 repair-window=100000
   a=rid:1 send pt=103;max-width=1280;max-height=720;max-fps=30
   a=rid:2 send pt=104;max-width=1280;max-height=720;max-fps=30
   a=rid:3 send pt=103;max-width=640;max-height=360;max-br=300000
   a=rid:4 send pt=104;max-width=640;max-height=360;max-br=300000
   a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
   a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
   a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
   a=rtcp-fb:* ccm pause nowait
   a=simulcast:send 1,2;3,4

                 Figure 8: Simulcast and Redundancy Example

WebRTC API 对于 Simulcast 的支持

WebRTC 提供了 RTCRtpEncodingParameters 作为接口,应用于 RTCRtpSender.

属性有

  • active

If true, the described encoding is currently actively being used. That is, for RTP senders, the encoding is currently being used to send data, while for receivers, the encoding is being used to decode received data. The default value is true.

  • codecPayloadType

When describing a codec for an RTCRtpSender, codecPayloadType is a single 8-bit byte (or octet) specifying the codec to use for sending the stream; the value matches one from the owning RTCRtpParameters object's codecs parameter. This value can only be set when creating the transceiver; after that, this value is read only.

  • dtx Deprecated

Only used for an RTCRtpSender whose kind is audio, this property indicates whether or not to use discontinuous transmission (a feature by which a phone is turned off or the microphone muted automatically in the absence of voice activity). The value is taken either enabled or disabled.

  • maxBitrate
    An unsigned long integer indicating the maximum number of bits per second to allow for this encoding. Other parameters may further constrain the bit rate, such as the value of maxFramerate or transport or physical network limitations.

  • maxFramerate
    A value specifying the maximum number of frames per second to allow for this encoding.

  • ptime
    An unsigned long integer value indicating the preferred duration of a media packet in milliseconds. This is typically only relevant for audio encodings. The user agent will try to match this as well as it can, but there is no guarantee.

  • rid
    A string which, if set, specifies an RTP stream ID (RID) to be sent using the RID header extension. This parameter cannot be modified using setParameters(). Its value can only be set when the transceiver is first created.

  • scaleResolutionDownBy
    Only used for senders whose track's kind is video, this is a double-precision floating-point value specifying a factor by which to scale down the video during encoding. The default value, 1.0, means that the sent video's size will be the same as the original. A value of 2.0 scales the video frames down by a factor of 2 in each dimension, resulting in a video 1/4 the size of the original. The value must not be less than 1.0 (you can't use this to scale the video up).

代码示例 https://www.fanyamin.com/webrtc/examples/local_peer_connection.html

for(const audioTrack of local_stream.getAudioTracks()) {
                    pc.addTransceiver(audioTrack, {direction:  WT.call.getDirection(), streams: [local_stream]});
                }

                var encodings = [
                    {rid: 'high', maxBitrate: 2500000, active: true, priority: "high"},
                    {rid: 'middle', maxBitrate: 1500000, active: true, scaleResolutionDownBy: 2.0},
                    {rid: 'low', maxBitrate: 100000, active: true, scaleResolutionDownBy: 4.0}
                ];

                for(const videoTrack of local_stream.getVideoTracks()) {
                    pc.addTransceiver(videoTrack, {direction:  WT.call.getDirection(), sendEncodings: encodings, streams: [local_stream]});
                }

参考资料

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐