WebRTC媒体协商01 流程介绍

一、媒体协商的重要性:

为了更好地说明这一点,我将通过一个小故事来解释:

想象一下,在一个战争时代,一名地下党员小李深入某个棉被厂充当的卧底。上级下达了一项刺杀任务,这引发了几个关键问题:

  • 刺杀目标是谁?(接收者信息)
  • 哪条路最为迅速?(路线选择)
  • 应当使用何种型号的子弹? 如果我使用的是三八大盖的枪,而你送来的却是汉阳造的子弹,那么就无法使用了。(传输内容信息)
  • 在传送过程中如何确保敌人无法发现子弹? 例如,可以将子弹藏在一个带锁的箱子内。(传输加密)

通过这个三流的故事,我们可以得出结论:要想与某人进行通信,我们需要了解以下几个关键点:

  • 接收者的位置在哪里?(IP 地址 + 端口号)
  • 哪条路径最为优化?(ICE + Candidate 选路)
  • 应当发送何种类型的“子弹”?(即呼叫者和被呼叫者的一些能力,例如对音视频编解码器的支持情况)
  • 如何确保在传输过程中“子弹”不会被发现?(即对传输内容进行加密)

通过这种“生动”的对比,媒体协商的重要性得以清晰展现。就是开始通话之前我们先互相了解下对方的条件(如:音视频编解码器、使用的传输协议、IP 端口和传输速率等等),这个条件是通过“信令服务器”以SDP的方式交换的。

二、WebRTC中媒体协商的流程:

下图中Amy是呼叫端,Bob是被呼叫端,中间Signaling Channel是一个他俩都能访问到的“信令服务器”,我们通过这个图说清楚媒体协商的过程。

在这里插入图片描述

  1. CreateOffer:Amy创建一个本地Offer,并通过信令服务器发送给Bob;
  2. SetLocalDescription:Amy同时将创建的本地Offer保存到自己的local_description中;
  3. setRemoteDescription:对Bob来说,收到的Amy Offer信息就是远端的信息,因此保存到自己的remote_description中;
  4. CreateAnswer:Bob创建一个Answer信息;
  5. SetLocalDescription:将Answer信息存储到本地的local_description当中,并开始协商;
  6. Bob将Answer发送给Amy;
  7. SetRemoteDescription:Amy收到Bob发送过来的Answer,存储到自己的remote_description当中,并开始协商;

备注:

  • 上面说的offer和answer都属于SDP信息,也就是一个json字符串,里面描述了自己的能力(编解码能力、协议、码率等);

  • 上面说的“协商”就指的是根据offer和answer,提取两者都支持的最优能力;

  • 媒体协商的最终结果就是,双方都创建了一个RTCPeerConnection(可以简单理解成一个超级socekt),后面就可以直接发送音视频数据了;

  • 还有一个非常重要的动作,就是收集candidate;(当然是并行同时发送Offer给对端,也就是说Offer里面并没有包含candidate,因为还正在收集)

  • Amy收到对端Answer才开始协商,Bob在发送Answer之前就开始协商了;

三、媒体协商前端代码:

上面知道了流程,如果我们在浏览器想使用webrtc的WebAPI,应该使用什么样的流程呢?

  1. 呼叫方创建并发送offer:

    var pcLocal = new RTCPeerConnection(opts1); 
    
    pcLocal.createOffer((offer)=>{    
      pcLocal.setLocalDescription(offer);  
      singalChannel.send(offer)
    }, handleError);
    
  2. 被呼叫方收到offer:

    var pcRemote = new RTCPeerConnection(opts2);
    
    signalChannel.on('message', (message)=>{  
      if(message.type === 'offer'){    
        pcRemote.setRemoteDescription(        
            new RTCSessionDescription(message)
        )  
      }
    })
    
  3. 被呼叫方创建并发送answer:

    pcRemote.createAnswer((answer)=>{  
      pcRemote.setLocalDescription(answer);  
      singalChannel.send(answer);
    }, handleError );
    
  4. 呼叫方收到answer:

    signalChannel.on('message',(message)=>{  
      if(message.type==='answer'){    
        pcLocal.setRemoteDescription(        
            new RTCSessionDescription(message)
        )  
      }
    })
    

四、媒体协商状态机:

为了提高媒体协商过程的可靠性、可维护性和灵活性,引入状态机管理协商状态;

1、状态转移图如下:

在这里插入图片描述

  • 记住,每次设置完成之后,只有我们的状态机回到“Stable”状态,这样下次才可以工作;

  • 状态机对呼叫方和被呼叫方都有一个;

  • 状态解释:

    • stable:当创建完RTCPeerConnection实例之后,便处于stable状态;
    • have-local-offer:当调用setLocalDescription之后,便成了have-local-offer状态;
    • have-remote-offer:被呼叫段收到呼叫端发送的offer之后;
    • have-local-pranswer:就是被呼叫端提前应答;一般是媒体数据没有准备好的时候,提前应答节约时间;那这个临时的answer有一个特点,就是它没有媒体数据也就是说没有音频流和视频流,并且将这个发送的方向设置成send only;这样,可以先进行ICE收集candidate,和进行DTLS握手;
    • have-remote-pranswer:呼叫端收到被呼叫端发送来的pranswer之后,设置为这个状态(记住此时非stable,不能操作RTCPeerConnection);
  • 状态机运行:

    • 理想情况下:呼叫方(1->2->3),被呼叫方(6->7->9)

      • 开始创建这个RTCPeerConnection的时候,处于stable状态,但是这个时候还不能进行编解码,因为没有进行媒体协商;

      • 呼叫方创建offer,并调用setLocalDescription将offer传进去,状态变化:stable -> have-local-offer;

      • 当被呼叫端收到offer之后,调用setRemoteDescription将offer设置进去,状态变化:stable->have-remote-offer;

      • 被呼叫方创建answer之后,调用setLocalDescription将answer设置进去,状态变化:have-remote-offer -> stable;

      • 呼叫方收到answer之后,调用setRemoteDescription将answer设置进去,状态变化:have-local-offer -> stable;

      • 这时候双方都回到stable状态了,因此就可以继续使用RTCpeerConnection来进行编解码、传输等操作了;

    • 特殊情况:呼叫方(1->2->4->5)被呼叫方(1->6->7->8)

      • 开始创建这个RTCPeerConnection的时候,处于stable状态,但是这个时候还不能进行编解码,因为没有进行媒体协商;
      • 呼叫方创建offer,并调用setLocalDescription将offer传进去,状态变化:stable -> have-local-offer;
      • 被呼叫方收到offer之后,进行setRemoteDescription,将offer设置到本地,状态变化:stable -> have-remote-offer;
      • 被呼叫方可能因为暂未收集好媒体数据,先生成一个pranswer,状态变化:have-remote-offer -> have-local-pranswer;
      • 呼叫方收到对端的pranswer之后,调用setRemoteDescription设置到自己本地,状态变化: have-local-offer -> have-remote-pranswer;
      • 这个时候双方可以提前走后续的ICE收集candidate、dtls等流程;
      • 被呼叫方收集好媒体数据之后,生成一个answer,调用setLocalDescription设置进本地,状态变化:have-local-pranswer -> stable;
      • 呼叫方收到对端的answer之后,调用setRemoteDescription将answer设置进本地,状态变化:have-remote-pranswer -> stable:
      • 这时候双方都回到stable状态了,因此就可以继续使用RTCpeerConnection来进行编解码、传输等操作了;

2、时序图:

在这里插入图片描述

1)上面主要分为三部分:协商部分、candidate收集部分、数据流部分

2)呼叫段发送Offer的同时,也会去向sturn/turn服务器发送bind request;

3)等sturn/turn收集完所有通路之后,就会通过onIceCandidate回复过来;

4)然后再A这边通过信令服务器发送candidate给B;

5)B将A的通路进行排序,然后选择最优的,然后,同时将candidate发送给A;

6)A收到之后也检测下连通性;

7)然后就通过P2P去穿越发送媒体流给B;

8)B收到之后不会立即显示,会通过onAddStream添加到本地,然后向上抛给渲染模块显示

五、SDP:

上面说的协商过程中发送的offer和answer都是一种SDP格式的信息,SDP 全称 SessionDescription Protocal,直译就是通用会话描述协议。

1、SDP结构:

在这里插入图片描述

  • 会话层:基本是对全局会话的说明,信息量比较小,包括会话的名称、目的、存活时间等;
  • 媒体层:媒体层非常重要,包括媒体格式、传输协议、ip和端口(webrtc没有使用这个,使用的是ice)、媒体负载类型;
  • 格式:json格式,由多条"="组成;这儿暂时不分析,直接分析webrtc里面具体的sdp;

2、WebRTC中的SDP:

webrtc中的SDP和标准的SDP规范有所不同,主要是按照功能划分了如下五大部分:

在这里插入图片描述

3、关键字段说明:

  • ICE-FULL和ICE-LITE:

    • 为了验证服务器给我的candidate是否正确,需要发送stun request,对端回复stun response;

    • ice-full客户端和服务端都要发送stun request,为了提高效率ice-lite只需要客户端发送即可;

    • 一般服务器都默认是ice-lite;

  • PlanB和UnifiedPlan:

    • PlanB 和 UnifiedPlan 其实就是 WebRTC 在多路媒体源(multi media source)场景下的两种不同的 SDP 协商方式。如果引入 Stream 和 Track 的概念,那么一个 Stream 可能包含 AudioTrack 和 VideoTrack,当有多路 Stream 时,就会有更多的 Track,如果每一个 Track 唯一对应一个自己的 M 描述,那么这就是 UnifiedPlan,如果每一个 M line 描述了多个 Track(track id),那么这就是 Plan B;

    • PlanB比较古老,除非考虑兼容性,否则直接用 UnifiedPlan;

4、实例分析:

// 【1】会话层
// sdp版本号为0
v=0
// owner的名字不写,会话id是7595655801978680453,会话版本是2(后面如果有类似改变编码的操作,sess-version加1),地址类型为IP4,地址为127.0.0.1(这里可以忽略)
o=- 7595655801978680453 2 IN IP4 127.0.0.1
// session名字为空
s=-
// 会话的起始时间,都为0表示一直存活
t=0 0
a=ice-lite
// 音频、视频的传输的传输采取多路复用,在底层传输层使用同一个通道尽心传输
a=group:BUNDLE 0 1
// WMS是WebRTC Media Stram的缩写,这里给Media Stream定义了一个唯一的标识符。一个Media Stream可以有多个track(video track、audio track),这些track就是通过这个唯一标识符关联起来的,具体见下面的媒体行(m=)以及它对应的附加属性(a=ssrc:)
a=msid-semantic: WMS 5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
// 【2】媒体层
// [2.1] 音频描述
// [2.1.1] m行
// 表示媒体是音频,可以向我的端口x发送数据(9其实表示这个端口无意义),采用UDP传输加密的RTP包,并使用基于RTCP的音视频feedback机制来提升传输质量,
// 后面的数字是payload type
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 126
// [2.1.2]a行,对前面m行补充说明
// 音频发送者的IP4地址,WebRTC采用ICE,这里的 0.0.0.0表示任意端口
c=IN IP4 0.0.0.0
// RTCP采用的端口、IP地址(webrtc同样没有使用)
a=rtcp:9 IN IP4 0.0.0.0
// ice-ufrag、ice-pwd 是ice协商时候使用,会话建立的时候我先把我的用户名密码发送给你,你建立会话之后再发送给我,
// 如果我校验不是我原来的用户名和密码,我就终止会话
a=ice-ufrag:58142170598604946
a=ice-pwd:71696ad0528c4adb02bb40e1
// 表示我使用的trickle协议,原来的是会话建立的时候,我先要将本地的所有candidate都收集完成,然后,再通过信令服务器交换给你,
// 现在是SetLocalDescription将我本地的发送给对端之后,我就同时开始收集本地candidate,我收集到一个发现比之前的好,
// 就替换掉之前的,循环往复;
a=ice-options:trickle
// 指纹,我先把我自己的指纹发送给你,待会儿通过dtls进行证书交换的时候,对方收到了证书之后,通过证书生成一个指纹,
// 和之前你发送的一样的话,表示这个会话是值得信任的,否则,不进行通话;
a=fingerprint:sha-256 7F:98:08:AC:17:6A:34:DB:CF:3B:EC:93:ED:57:3F:5A:9E:1F:4A:F3:DB:D5:BF:66:EE:17:58:E0:57:EC:1B:19
// 表示我们谁作为服务端,谁作为客户端,由你(answer)说了算
a=setup:actpass
// 当前媒体行的标识符(在a=group:BUNDLE 0 1 这行里面用到,这里0表示audio)
a=mid:0
// RTP允许扩展首部,这里表示采用了RFC6464定义的针对audio的扩展首部,用来调节音量,比如在大型会议中,有多个音频流,就可以用这个来调整音频混流的策略
// 这里没有vad=1,表示不启用这个音量控制
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
// 表示既可以发送音频,也可以接收音频
a=sendrecv
// 表示我支持rtp和rtcp端口复用
a=rtcp-mux
// 下面几行都是对audio媒体行的补充说明(针对111),包括rtpmap、rtcp-fb、fmtp
// 表示我对下面将对前面payload type为111的进行说明
a=rtpmap:111 opus/48000/2
// rtcp-fb:基于RTCP的反馈控制机制
a=rtcp-fb:111 transport-cc
a=rtcp-fb:111 nack
// 最小的音频打包时间
a=fmtp:111 minptime=20
// 跟前面的rtpmap类似
a=rtpmap:126 telephone-event/8000
// ssrc用来对媒体进行描述,格式为a=ssrc:<ssrc-id> <attribute>:<value>,具体可参考 RFC5576
// cname用来唯一标识媒体的数据源
a=ssrc:16864608 cname:YZcxBwerFFm6GH69
// msid后面带两个id,第一个是MediaStream的id,第二个是audio track的id(跟后面的mslabel、label对应)
a=ssrc:16864608 msid:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV 128f4fa0-81dd-4c3a-bbcd-22e71e29d178
a=ssrc:16864608 mslabel:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
a=ssrc:16864608 label:128f4fa0-81dd-4c3a-bbcd-22e71e29d178
// 跟audio类似,不赘述
m=video 9 UDP/TLS/RTP/SAVPF 122 102 125 107 124 120 123 119
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:58142170598604946
a=ice-pwd:71696ad0528c4adb02bb40e1
a=fingerprint:sha-256 7F:98:08:AC:17:6A:34:DB:CF:3B:EC:93:ED:57:3F:5A:9E:1F:4A:F3:DB:D5:BF:66:EE:17:58:E0:57:EC:1B:19
a=setup:actpass
a=mid:1
a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:4 urn:3gpp:video-orientation
a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=sendrecv
a=rtcp-mux
// 正常的rtcp包括对端带宽估计、丢包信息等,这表示可以为了节省带宽,你只发送丢了哪些包即可;
a=rtcp-rsize
// 表示我对下面将对前面payload type为122的进行说明
a=rtpmap:122 H264/90000
// 表示在 SDP 中为 RTP 负载类型为122的媒体流启用了对 FIR 的 RTCP 反馈机制,用于接收端向发送端请求发送关键帧
a=rtcp-fb:122 ccm fir
// 表示在 SDP 中为 RTP 负载类型为122的媒体流启用了 NACK 的 RTCP 反馈机制,用于在传输过程中检测和处理丢包情况
a=rtcp-fb:122 nack
// 用于在传输过程中检测丢包情况,而 PLI 是 Picture Loss Indication 的缩写,用于指示接收端需要一个关键帧(I帧)
// 来正确解码和显示视频。当接收端检测到丢包或图像帧损坏时,它可以发送NACK反馈请求重新发送丢失的数据包,
// 并且发送 PLI 请求以请求发送端发送一个关键帧
a=rtcp-fb:122 nack pli
// 表示在 SDP 中为 RTP 负载类型122配置了Google提出的 REMB 反馈机制,
// 用于接收端向发送端报告估计的最大比特率,以优化传输和编码参数的调整
a=rtcp-fb:122 goog-remb
// 在 SDP 中为 RTP 负载类型为 96 的媒体流启用了 Transport-CC RTCP 反馈机制,用于实现传输方向上的拥塞控制
a=rtcp-fb:122 transport-cc
a=fmtp:122 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
// 表示在 SDP 中定义了 RTP 负载类型为102的媒体流,并指定了使用 RTX 作为重传机制
a=rtpmap:102 rtx/90000
// 表示在 SDP 中为 RTP 负载类型为102的媒体流定义了特定的参数设置,其中指定了负载类型102是负责重传负载类型122的数据包。
// 这种设置通常与 RTX(Retransmission Transmission)相关联,用于处理数据包的重传机制
a=fmtp:102 apt=122
a=rtpmap:125 H264/90000
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
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:124 H264/90000
a=rtcp-fb:124 ccm fir
a=rtcp-fb:124 nack
a=rtcp-fb:124 nack pli
a=rtcp-fb:124 goog-remb
a=rtcp-fb:124 transport-cc
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=124
a=rtpmap:123 H264/90000
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 transport-cc
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=123
a=ssrc-group:FID 33718809 50483271
a=ssrc:33718809 cname:ovaCctnHP9Asci9c
a=ssrc:33718809 msid:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV 1d7fc300-9889-4f94-9f35-c0bcc77a260d
a=ssrc:33718809 mslabel:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
a=ssrc:33718809 label:1d7fc300-9889-4f94-9f35-c0bcc77a260d
a=ssrc:50483271 cname:ovaCctnHP9Asci9c
a=ssrc:50483271 msid:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV 1d7fc300-9889-4f94-9f35-c0bcc77a260d
a=ssrc:50483271 mslabel:5Y2wZK8nANNAoVw6dSAHVjNxrD1ObBM2kBPV
a=ssrc:50483271 label:1d7fc300-9889-4f94-9f35-c0bcc77a260d
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值