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,上面有一些丰富的接口,虽然这些接口都比较"高级"(高级并不是一个褒义词,高级就意味着只能粗粒度地设置,不能更精细化),但后面肯定会开放更多的底层能力。

参考文档:

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb6 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角6 小时前
CSS 颜色
前端·css
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me8 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者8 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架