ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

mediasoup中Simulcast与SVC:分层编码、选路机制与带宽估计实战

mediasoup中Simulcast与SVC:分层编码、选路机制与带宽估计实战 做流媒体自建服务这么长时间对接mediasoup的时候simulcast和svc这两个词几乎天天见。但说句实话它们本身不难理解难的是搞明白它们在mediasoup里面到底怎么流转、怎么选路、怎么和带宽估计配合。如果你也卡在这一块这篇文章应该能帮你把这块拼图补上。这其实是我们流媒体学习之路系列的第7篇前面聊过mediasoup的基础架构、Router/Transport/PipeTransport这些核心概念也梳理过RTP和RTCP的调协细节。这次我把simulcast和svc放在一起讲是因为它们在SFU场景里解决的是同一个问题网络带宽不均衡时怎么给不同的接收端分发不同质量的视频流。但实现思路完全不同踩坑姿势也完全不一样。1. simulcast与svc两种不同的视频分层思路1.1 simulcast多路独立编码接收端按需挑一路simulcast翻译过来叫“联播”本质上是发送端同时编码产生多路分辨率、码率、帧率都不相同的视频流每一路都是独立编码、可单独解码的完整视频流。比如一个1080p的信号源可以先出三层1080p高层、720p中层、360p低层然后同时往SFU推这三路。在WebRTC和mediasoup里这三路流通过不同的SSRC来区分同时通过RTP扩展头里的ridRTP Stream ID字段作为每路流的“身份证”。接收端想看清楚一点就问SFU要高层网络不好就问SFU要低层切换阈值由应用层决定。SFU不做任何转码只是按需转发其中的某一层。这样做的优势非常明显SFU不需要转码延迟低、CPU消耗少。各层之间的质量差别很稳定高层清晰低层流畅。但它也有一个先天短板——带宽利用效率低。因为发送端要同时编码并上推多路流实际上行带宽是“各层码率之和”而不是单一编码一路。如果在1对1通话场景里接收端只会用到其中一路其他层完全是白白浪费。所以业内才有“simulcast费上行带宽”这个说法。1.2 svc一层基础层叠加增强层按需丢弃部分包SVCScalable Video Coding可伸缩视频编码换了一种思路只编码一路视频流但这路流内部天然分层级。最低层通常叫基础层base layer只有它才能独立解码图像质量最低、分辨率最低、帧率也最低。在基础层之上还有若干增强层比如空间增强层可以拉高分辨率时间增强层可以补帧率质量增强层可以降量化步长提升画质。SVC在RTP传输中的做法是整条流仍然只有一个SSRC和一路RTP包但每个包通过SVC规范里定义的RTP扩展头比如时间层的tsid、空间层的tlid来标识自己属于哪个层。接收端或SFU拿到包之后可以按层级做筛选丢弃。比如说当前网络只能撑住基础层SFU就把所有增强层的包全部丢掉只转发基础层的包接收端照样能解码出视频只是分辨率低、画质差点。SVC最大的优点是带宽利用率高因为发送端只需要编码一条流增强层的码率在必要时候才真正被转发出去不会像simulcast那样早早上行带宽全都占满。它的主要缺点也很现实编码器复杂度高CPU占用比simulcast高不少而且终端支持不统一比如某些客户端对H.264 SVC压根不支持VP9和AV1的SVC支持情况也因平台而异。1.3 二者最本质的差异空间冗余的取舍如果用生活化的方式理解simulcast像是一家餐厅同时准备小份、中份、大份三个独立菜品顾客要哪个就给哪个没点的那份就白白在厨房里放着换菜速度很快。SVC像是一个多层蛋糕第一层是可以单独吃的上面的每一层必须叠加在下面才能吃服务员只会把客人够得到的部分端上去但蛋糕本体做得更复杂。从SFU的角度看simulcast转发的是“独立流”的某个分支SVC转发的是“同一条流”中的部分包。这两种机制在媒资系统里都对RTCP带宽估计敏感但在mediasoup的具体实现上有非常大的区别接下去我会把底层机制展开讲。2. 深入mediasoup的simulcast与svc处理机制2.1 mediasoup怎么识别分层流mediasoup内部设计了一套逻辑来统一处理simulcast和SVC。它把每一路可辨识的层抽象成“空间层”Spatial Layer和“时间层”Temporal Layer两个维度然后用一个Layer集合来跟踪。对于simulcast发送端上推的三路独立视频流在mediasoup看来就是三个空间层。每一层有自己的SSRC、独立RTP序列号空间和独立码率统计。时间层的概念在simulcast里也存在比如VP8的动态时间层、H.264的某些编码器实现会在一路流里再做时间分层但simulcast的时间层不像SVC那么突出。对于SVCmediasoup在接收端解析RTP包里的层标识扩展头把包归入不同空间层和时间层。SVC和一个SSRC配对所有层的包都混在这一个流里mediasoup必须通过RTP头精确区分。以VP9 SVC为例同一个SSRC的RTP包既可能有空间层0的时间层0也可能有空间层1的时间层1各自对应不同的RTP头扩展字段值。mediasoup在做转发时不是简单粗暴地把所有包都转发给consumer而是通过consumer的targetLayer来确定当前要转发的空间层和时间层然后只转发该层及其下层的包。对simulcast来说如果consumer选了空间层2那mediasoup只转发空间层2那条SSRC的包。对SVC来说如果consumer选了空间层1时间层1mediasoup需要同时转发空间层0和空间层1中不高于时间层1的包因为空间层1的解码依赖基础层。2.2 RTP扩展头与SDP协商分层的“身份证”在这个体系里RTP扩展头是分层的核心标识。mediasoup能够正确识别simulcast和SVC完全取决于发送端是否在SDP协商阶段带上对应的RTP扩展头并且在实际的RTP包里填上了正确的值。simulcast场景使用的扩展头主要是urn:ietf:params:rtp-hdrext:sdes:rid也就是我们常说的rid扩展头。发送端在编码完三层流之后在每一层的RTP包里把rid字段写成对应的Stream ID。mediasoup拿到SDP时通过arid和asimulcast属性来重建发送端的simulcast场景。SDP里simulcast的典型描述长这样arid:f1 send arid:f2 send arid:f3 send asimulcast:send f1;f2;f3SVC则需要借助其他扩展头比如VP9 SVC里tsid和tlid或者是后来标准的http://www.webrtc.org/experiments/rtp-hdrext/vp9-tl0等。SDP里同样会通过asvc等属性来声明。mediasoup对这些扩展头的解析实现在自家的RtpStream和Producer里面所以如果你要自定义扩展头不走offer里的扩展头协商机制mediasoup是识别不出来的。在WebRTC的常规协商流程里SDP的offer和answer都是自动生成的但用户要确保编码器设置正确。你要是配置了simulcast但没开启rid那mediasoup收到的还是一路平坦的视频流根本没法选层。同理SVC没带好层标识头mediasoup也无法感知这是SVC。2.3 WebRTC里的Simulcast流程从浏览器到mediasoup正常情况下一个基于mediasoup的WebRTC应用浏览器端的流转到mediasoup的路径是这样的浏览器端先用getUserMedia拿到摄像头流然后通过RTCPeerConnection创建发送端。这里的关键是addTransceiver或者setParameters的encodings数组数组里的每一项就对应一个simulcast层。浏览器在本地编码器里会尝试生成多个编码层并在SDP里带着对应的rid和simulcast声明发出去。mediasoup收到offer之后先是调协出公用的RTP能力再根据SDP信息创建RtpStreamRecv解析出每层的SSRC、rid、码率参数然后通知应用层“这个Producer有3个空间层”。应用层通过router.createProducer创建producer把对应的rtpParameters保存下来。这个时候Consumer端来了一个接收者mediasoup会通过RTP参数协商为接收者生成一个Consumer。如果接收者网络状况好应用层会把Consumer的targetLayer设置为空间层2如果网络不佳设为空间层1或0。mediasoup收到指令后就只转发对应层所在SSRC的包。整个流程看起来不复杂但每一环都有很多细节要扣。比如浏览器端是否真的开启了编码器的多路同时编码码率控制模式用的CBR还是VBR关键帧间隔多少都会直接影响后续的选层体验。3. 实操把simulcast跑起来3.1 Producer端配置encodings声明三层在mediasoup中最核心的就是Producer端的encodings配置。我曾经在一个自建流媒体服务项目里把1080p的视频源做成了三层simulcast当时用的mediasoup-client配置如下const producer await sendTransport.produce({ track, encodings: [ { maxBitrate: 1500000, scaleResolutionDownBy: 1 }, { maxBitrate: 500000, scaleResolutionDownBy: 2 }, { maxBitrate: 150000, scaleResolutionDownBy: 4 } ], codecOptions: { videoGoogleStartBitrate: 800 } });这里的scaleResolutionDownBy是mediasoup-client基于原始分辨率做等比缩放的核心参数。原始1080p第一次不缩放第二次缩小一半成540p第三次缩到270p。实际跑起来浏览器会开启三个独立的编码器实例同时工作输出的三层视频流在SDP里对应三个SSRC、三个rid。如果你的上层应用不是浏览器而是自研客户端直接对接mediasoup那rtpParameters要自己手动组织encodings数组里的每个对象要手动填好rid、maxBitrate、scaleResolutionDownBy等字段。在非浏览器环境里还要特别注意编码器是否支持多路并发输出很多硬编码器虽然支持simulcast但层数限制为2层这个时候强行配3层会导致编码失败。3.2 Consumer端选层与切层Consumer端在mediasoup里最简单的选层操作是直接指定空间层和时间层。框架层面mediasoup提供了一套清晰的API比如consumer.setPreferredLayers({ spatialLayer: 1, temporalLayer: 2 })。实际使用中我们在视频会议场景里会在每个consumer上监听带宽估计事件一旦检测到接收端下行带宽充足就把preferredSpatialLayer提高到2让用户看到高清画面带宽吃紧时先降时间层再降空间层这样画面模糊但不会卡顿。在mediasoup的消费者选项中也有preferredSpatialLayer和preferredTemporalLayer的初始化参数配合上面的setPreferredLayers可以在创建consumer时就设定默认层。有一个关键点需要注意mediasoup的consumer选层指令是实时的但切换的生效时机取决于关键帧。接收端要从低层切到高层必须等高层那个SSRC产生一个关键帧才可能无损切换。所以很多生产级应用会让编码器在simulcast三路上周期性产生关键帧避免切层时出现长时间花屏。我们当时把关键帧间隔设在2秒既保证带宽不太浪费又能快速切层。3.3 带宽估计与选层策略的配合单纯做了simulcast选层还不够真正让系统自适应网络的核心在于带宽估计。mediasoup内置了transport-cc带宽估计机制它能够在SFU侧估算出每一条Transport的可用带宽但默认情况下它不会自动帮你切层。切层逻辑需要应用层自己实现。我在实际项目里是这么做的首先在mediasoup的worker里开启了transport-cc并且在各个consumer上监听score事件。mediasoup会定期为每个consumer的层评分包括空间层分数、时间层分数和编码层分数。一旦评分明显下降说明当前层码率超出了接收端带宽我会立刻把preferredSpatialLayer降一级再观察评分是否恢复。这套逻辑在协议上依赖RTCP Receiver Report和Transport-CC反馈所以你们要在创建router的时候把enableTcp设为true同时确保rtcp配置合理。不开启transport-ccmediasoup照样能跑simulcast但选层就只能靠拍脑袋了。4. simulcast还是svc我该怎么选4.1 带宽消耗与计算对比如果要从带宽维度做选型必须先搞清楚simulcast和SVC的码率构成。simulcast下发送端的实际上行带宽大约是各层码率之和。假设11080p高层2.5Mbps、720p中层1Mbps、360p低层300kbps那上行就是约3.8Mbps。而下行带宽取决于接收端选中的层。要是三个人同时收高层每人都消耗2.5MbpsSFU的出口就是7.5Mbps。如果是SVC发送端编码一条总码率约3.5Mbps的流上行就是这个数不需要为每个层单独付出完整码率。下行方面如果要1080pSFU转发全部约3.5Mbps的包如果只能收360p只需要转发基础层的几百kbps包。同等画质下SVC的总码率一般能比simulcast节省20%到40%具体消耗还得看内容复杂度、编码器实现和层间相关度。但对于包含大量会议场景的多人视频simulcast的上行浪费是绕不开的痛。4.2 编码复杂度与终端支持再看编码与终端的适配。simulcast的编码复杂度最低因为每一路就是一次常规编码多个编码器并行工作。但编码器数量多CPU使用率会上来。好在现代浏览器对simulcast的编码支持已经非常成熟主流平台WebRTC的simulcast都稳定。SVC起步晚目前主要在VP9和AV1上表现较好尤其是VP9的SVC模式在很多WebRTC引擎已经可用。如果你用的是H.264SVC的支持就惨不忍睹了因为H.264 SVC的profile在WebRTC和硬编码器里基本是被冷落的。所以我做一个产品线建议如果是纯WebRTC的会议场景优先simulcast它生态成熟、问题容易排查如果是直播、云转码或者存储边界弱化的场景SVC优势更明显带宽利用率高、端到端延迟低。4.3 实际项目里的选型建议我接手的几个自建流媒体服务项目里最后都是simulcast为主力。原因很简单客户端的兼容性最稳编码器不用搞复杂的层级设置开箱即用。而SVC我更多放在一些对带宽极其敏感的1对1通话和移动端弱网场景里。如果你新项目要确定方案先从这两个问题出发第一你的终端设备多样吗是否有老平台第二你的瓶颈在上行还是下行。如果上行带宽宽裕但下行各异simulcast最合适。如果上行本来就不够且客户端都能支持VP9/AV1 SVC那SVC值得赌一把。另外mediasoup实际上支持simulcast和SVC共存你可以让同一个router里的不同producer用不同模式但为降低排查复杂度生产环境建议同一类终端统一切到一种模式。5. 常见问题与排查技巧5.1 simulcast流“选不了层”怎么办这是我在做mediasoup联调时最常遇到的问题。表现是producer明明跑了三层simulcast但consumer创建后设preferredSpatialLayer怎么都不生效或者只看到一层。排查顺序一般是先看SDP。确认offer里有没有asimulcast:send有没有rid声明。如果没有就能确定发送端根本没有协商好simulcast。其次看mediasoup侧producer的score事件看看各层评分是否都在正常输出。如果某一层评分特别低可能是那层编码器没有产生足够的关键帧或者码率设置太低导致画面静止。还有一个容易忽略的细节simulcast的编码器需要手动开启“关键帧同时生成”功能。部分浏览器默认只有高层会周期出关键帧低层只在初始时出一次。这种情况下你从高层切到低层没问题但从低层切回高层就会等待一个很不确定的关键帧周期造成切层后卡片几秒。建议在编码器配置里对各层设置关键帧间隔。5.2 SVC在mediasoup中的限制SVC要跑通除了编码器支持mediasoup本身也要能识别层信息。在部分mediasoup版本里SVC的consumer创建成功不等于你可以直接选层。你会发现有些时候设置preferredSpatialLayer没有反应原因是RTP扩展头没协商上比如sdes:rtp-stream-id只对simulcast的rid有效SVC得用tsid和tlid那一套扩展头。所以排查SVC时要重点看协商出的RTP capabilities里扩展头是否包含SVC相关的uri。如果终端和mediasoup没达成一致那就只能当普通单流处理没有任何分层收益。另外记住SVC不能和simulcast在同一路producer里混用这两个模式在SDP里是互斥的你声明了simulcast就别想同时有SVC层标识。5.3 切层瞬间的体验优化最后分享一个切层体验优化的技巧。simulcast切层最怕什么不是切不到而是切了之后糊了很久。这通常是因为编码器关键帧在切换目标层上过慢。我们线上方案是给三层同时开启如下编码器选项videoKeyFrameInterval2同时启动videoKeyFrameForce逻辑在用户主动切层的指令到达SFU后通过信令通知浏览器那边的编码器立刻产生一次关键帧。这样切层延迟能压到半秒以内。还有一个小技巧是针对Consumer端的缓存在mediasoup里切层前先不要立刻丢弃当前层的包让接收端保持一个关键的缓冲周期等新层的关键帧到达后再做切换画面会平滑很多。这个和播放器的追帧策略有点像总的思路就是“先并行后切换”。流媒体这条路学的时候觉得每个点都懂联调的时候到处都是坑。simulcast和SVC是SFU里最核心的两个武器选对场景、配置对参数、做好切层策略整个视频服务的体验立马不一样。上面这些内容都是我在实际项目里的经验总结希望能帮你少踩几个坑也欢迎在评论区交流你们的选型方案。
返回列表