WebRTC sdp杂谈

sdp(Session Description Protocol)是一种非常古老的协议,最早见于rfc2327(1998年),被用于会议系统中通话双端的网络属性及媒体能力属性。2010年Google收购了VoIP软件开发商Global IP Solutions的GIPS引擎,改名为WebRTC,WebRTC选择了sdp协议为通讯双端媒体能力协商协议。

本文并不会重复造轮子去解释协议的内容,而是在了解sdp基础上去学习下相关API,错误处理,解析Google Meet项目与webrtc-samples项目,看看在浏览器端通过修改sdp(munged sdp)可以实现哪些功能。WebRTC项目提供能力非常多,但浏览器暴露的API又特别少,经过多年大家的经验(主要是看Google Meet是怎么用的),总结出修改sdp就可以打开WebRTC的底层能力。

一、sdp解析

sdp协议本身并不复杂,全部内容由<type>=<value>组成,最新协议描述见于rfc8866,网络上已经有非常多大神做了sdp协议的详细解析,如:

二、sdp相关的API与状态机

和sdp相关的API基本只有 createOffer、createAnswer、setLocalDescription、setRemoteDescription,虽然比较少,但加上本端、远端,类型为offer、answer这些因素开始接触的时候可能感觉比较绕。简单来说createOffer和createAnswer都是在各自本地生成sdp,然后通过setLocalDescription设置,接收到远端的sdp后通过setRemoteDescription设置。

pc每调用setLocalDescription、setRemoteDescription,本地的pc.signalingState就会产生对应的变化,完整的状态机如下,但have-local-pranswerhave-remote-pranswer是基本不用的,所以可以看下面简化的图。

捕获错误及处理策略

设置sdp错误大概可以分为两类:状态机错误和sdp信息不匹配,下面举例

case1: 如果不按状态机时序设置的话,调用setLocal/setRemote API会直接抛错,错误信息一般会指明错误信息。比如A/B用户同时创建offer给对方发送,那在A的视角来看,先setLocal -> have-local-offer -> setRemote -> throw error。

rust 复制代码
// 错误信息
DOMException: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': 
    The SDP does not match the previously generated SDP for this type

case2: 又例如A/B进行媒体协商,协商结果为空时也会报错:

csharp 复制代码
// 错误信息
Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': 
    Failed to parse SessionDescription. m=video 9 UDP/TLS/RTP/SAVPF  Invalid value: .

case3: 又或者在unified-plan格式下,offer/answer双方的mid要对齐:

vbnet 复制代码
// 错误信息
Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: 
    The order of m-lines in answer doesn't match order in offer. Rejecting answer

设置sdp报错一般发生在调试阶段,看到错误信息后结合offer及answer可以分析出错误在哪里,如何处理,前提还是需要熟悉sdp的内容。

如果你仍然不知道浏览器的报错信息指向了什么,可以查一下报错信息的代码,分析下源码引用的位置,比如上面case3的报错的函数注释与实现:source.chromium.org/chromium/ch...

完美协商

完美协商其实就是为了解决上面的case1,即两端同时在have-local-offer的状态收到了对方的offer,此时就会形成一个死结,当然我们可以从业务流程上避免此类情况发生,其实WebRTC也提供了解决方案,需要一端(称为polite)将have-local-offer的状态回滚到stable,另一端(称为impolite)拒绝掉/丢弃掉对方的offer,等polite完成协商后会产生一个新的answer给到impolite端去设置。

这里说到的polite与impolite也是由业务状态控制的,然后再看下setLocalDescription API,它还有2个作用:

  • 回滚到stable状态:pc.setLocalDescription({ type: 'rollback' }),这样signalingState就可以回滚到stable
  • 自动生成本地sdp与设置:pc.setLocalDescription() 不传参就可以认为是下面行代码的结合:这样写的好处是本地不需要关注自己应该去生成offer或者是answer,可以由状态机自己去判断
ini 复制代码
const oa = pc.createOffer(); // or pc.createAnswer();
await pc.setLocalDescription(oa);

当然这里只是简单介绍了下完善协商,扩展阅读可以看这里:

三、munged sdp

munged sdp就是在浏览器生成后,我们手动修改,加工一下,目的就是开启一些隐藏能力

JS生成使用sdp的过程(伪代码):

scss 复制代码
const pc = new RTCPeerConnection();
// 采集
pc.addTrack(videoTrack);
// 生成offer类型的sdp
const offer = await pc.createOffer(); // { type: 'offer', sdp: 'xx' }
// --> munging sdp
// 将生成的sdp设置到pc中
pc.setLocalDescription(offer);

可以看到sdp生成后可以拿到sdp,这样给了我们修改的空间,munging sdp也就是发生在这个阶段。

那么如何知道其他WebRTC应用修改了sdp,并快速找到修改了哪些sdp内容呢?可以借助一下Chrome浏览器提供的WebRTC调试工具:chrome://webrtc-internals(调试工具的玩法见:WebRTC Internals工具在项目中的实践 - 掘金),在其中可以找到createOfferOnSuccess和setLocalDescription(munged),如果有munged标记,那就说明sdp被手动修改了。

修改的内容,通过复制createOffer的sdp与setLocalDescription的sdp,使用\r\n换行符分割,再利用git等版本比较工具轻松地找到。

下面会举几个通过修改sdp实现的功能,有些是提供了API修改,有些则是需要直接修改sdp的字符串。

除这几个例子外,修改sdp可以解锁更多的功能,这个需要长时间积累和学习下优秀的项目

simulcast

simulcast是非常实用的功能,也是机智的小伙伴最早在Google Hangouts(Google Meet的前身)中利用给自己留的后门打开simulcast。具体改动为:先正常扩展1到2组video的ssrcs(ssrc、rtx、FID),然后使用a=ssrc-group:SIM ssrc1 ssrc2 ssrc3关联起来,设置成功后,浏览器会按输入分辨率的1/2/4比例发送simulcast,同时可以在webrtc-internals里看到有多层码流在发送:

注意,这种打开simulcast的方式为Chrome自行实现的方案,WebRTC已经有了标准API打开simulcast,所以并不是推荐使用这种方式,仅作为一种了解即可。API方式如下:

php 复制代码
pc.addTransceiver(stream.getVideoTracks()[0], {
    direction: "sendonly",
    streams: [stream],
    sendEncodings: [
      { rid: "h", maxBitrate: 1200 * 1024 },
      { rid: "m", maxBitrate:  600 * 1024, scaleResolutionDownBy: 2 },
      { rid: "l", maxBitrate:  300 * 1024, scaleResolutionDownBy: 4 }
    ]
  });
  
// 它生成的sdp长这样
a=rid:h send
a=rid:m send
a=rid:l send
a=simulcast:send :h;m;l

带宽相关

参考Peer connection: adjust bandwidth可以看到(实际在rfc里也有写)在sdp中给对应m section下面写入b=as就可以限制带宽,但在代码上面也能看到通过API的方式,所以实际b=as不会用到。

API方式如下:

ini 复制代码
const sender = pc1.getSenders()[0];
const parameters = sender.getParameters();
if (!parameters.encodings) {
  parameters.encodings = [{}];
}
if (bandwidthInKbps === 'unlimited') {
  delete parameters.encodings[0].maxBitrate;
} else {
  parameters.encodings[0].maxBitrate = bandwidthInKbps * 1000;
}
return sender.setParameters(parameters);

除此之外,还在3个google自己实现的和带宽相关的控制x-google-min-bitrate/x-google-max-bitrate/x-google-start-bitrate

在开源项目mediasoup中可以看到其应用:控制初始发送码率

ini 复制代码
m=video 7 UDP/TLS/RTP/SAVPF 96 97 (31 more lines) mid=1 c=IN IP4 127.0.0.1
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000
a=fmtp:96 x-google-start-bitrate=1000

视频编码

参考change-codecs,可以看到在选择视频编码时,可以通过修改answer里的codec的顺序来控制上行端的编码。

用到的办法是兼容性不太好的setCodecPreferences API,如果没有这个API的话,也可以手动改sdp的内容,主要是m line里codec id的顺序,想要使用的codec放到最前面,下面的rtpmap也放到前面(忘了是不是必须的了)。

三、plan-b与unified-plan

plan-b与unified-plan是sdp的两种描述语义格式,plan-b是Google自己的标准,WebRTC的标准就是unified-plan,Chrome浏览器也在M96正式废弃了plan-b的语义。

unified-plan的优势在于每个M section都可以有单独的配置,甚至不同的编码,但是带来的副作用就是导致sdp过大,在传输上会变慢,不过不过,现在各个WebRTC应用也不会传全量的sdp信息,比如开源项目media-soup,会在刚进房时协商好客户端和服务端的媒体能力,在后面发布或者订阅时只传必要的信息,减少传输数据量(突然想到三体里的一句:只送大脑)。

配合unified-plan的话,我们就可以使用Transceiver API了,这个在plan-b下是无法使用的,Transceiver可以认为对应一个m section,上面有一些丰富的接口,虽然这些接口都比较"高级"(高级并不是一个褒义词,高级就意味着只能粗粒度地设置,不能更精细化),但后面肯定会开放更多的底层能力。

参考文档:

相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css