19、SDP交换:创建Offer/Answer、设置本地/远程描述、ICE候选者收集

聊到WebRTC,绕不开的一个核心环节就是SDP交换。说白了,这就是两个浏览器之间互相“递名片”的过程——你得告诉对方你能支持什么格式、用什么编码、网络情况怎么样。我刚开始接触WebRTC时,总觉得SDP就是一堆看不懂的字符串,后来踩了不少坑才明白,这玩意儿是整个连接建立的基石。

什么是SDP?

SDP全称是Session Description Protocol,会话描述协议。它不负责传输数据,只负责描述“我想怎么跟你通信”。

一个典型的SDP里包含这些信息:

  • 媒体类型:音频还是视频?
  • 编解码器:用H264还是VP8?
  • 网络信息:IP地址、端口号
  • 传输协议:RTP还是SRTP?
  • 带宽信息:我能承受多少码率?

嗯,这里要注意:SDP本身只是一个文本格式的协议,WebRTC用它来协商双方的媒体能力。你想想看,如果两个人各说各的方言,那肯定没法交流。SDP就是帮你们统一“语言”的。

核心要点:SDP交换是WebRTC连接建立的“握手”阶段,没有这个步骤,后续的音视频数据根本传不过去。

创建Offer/Answer

整个SDP交换流程分为两步:发起方创建Offer,接收方返回Answer。我习惯把这个过程比作打电话——你先说“喂,我能说中文和英文”,对方回答“好的,那咱们说中文吧”。

创建Offer

在WebRTC中,创建Offer是通过RTCPeerConnection对象完成的。代码其实很简单:

const pc = new RTCPeerConnection(configuration);

// 创建Offer
pc.createOffer()
  .then(offer => {
    return pc.setLocalDescription(offer);
  })
  .then(() => {
    // 此时localDescription已经设置好了
    // 可以把offer通过信令服务器发送给对方
    signalingServer.send(JSON.stringify({
      type: 'offer',
      sdp: pc.localDescription.sdp
    }));
  })
  .catch(err => {
    console.error('创建Offer失败:', err);
  });

我在项目中遇到过一个问题:createOffer必须在添加了媒体轨道之后调用,否则生成的SDP里没有媒体信息,对方根本不知道你要传什么。所以记得先调用addTrack或者addTransceiver

创建Answer

接收方收到Offer后,需要创建Answer:

// 收到对方的Offer
signalingServer.onmessage = async (event) => {
  const data = JSON.parse(event.data);
  
  if (data.type === 'offer') {
    // 设置远程描述
    await pc.setRemoteDescription(new RTCSessionDescription(data));
    
    // 创建Answer
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
    
    // 把Answer发回去
    signalingServer.send(JSON.stringify({
      type: 'answer',
      sdp: pc.localDescription.sdp
    }));
  }
};

这里有个坑:setRemoteDescription必须在createAnswer之前调用。我曾经因为顺序搞反,折腾了半天才发现是这个问题。

设置本地/远程描述

设置描述其实就是把SDP信息告诉RTCPeerConnection对象。它有两个方法:

  • setLocalDescription():设置本地的SDP(Offer或Answer)
  • setRemoteDescription():设置对方的SDP

为什么要分本地和远程?因为WebRTC需要知道“我能做什么”和“对方能做什么”,然后才能找到共同点。说白了,就是双方各退一步,找个都能接受的方案。

小技巧:设置描述后,可以通过pc.localDescriptionpc.remoteDescription查看当前的状态。调试时我经常打印这两个属性,看看SDP里到底写了什么。

ICE候选者收集

SDP交换完成后,接下来就是ICE候选者收集。ICE全称是Interactive Connectivity Establishment,交互式连接建立。它的作用是帮双方找到一条可以通信的网络路径。

为什么会需要这个?因为现实网络环境很复杂——有NAT、有防火墙、有代理服务器。你想想看,如果两个设备都在不同的内网里,它们怎么直接通信?ICE就是来解决这个问题的。

ICE候选者类型

类型 说明 优先级
host 本机IP地址,比如192.168.1.100 最高
srflx 经过NAT映射后的公网IP 中等
relay 通过TURN服务器中转 最低

ICE收集的过程是这样的:

  1. 先收集本机所有网络接口的IP地址(host类型)
  2. 通过STUN服务器获取公网IP(srflx类型)
  3. 如果前两种都不行,就用TURN服务器中转(relay类型)

我曾经在一个客户的现场遇到过这种情况:他们的网络策略非常严格,host和srflx都连不上,最后只能靠relay。还好提前配置了TURN服务器,不然项目就黄了。

监听ICE候选者事件

在代码里,ICE候选者是通过事件回调来获取的:

pc.onicecandidate = (event) => {
  if (event.candidate) {
    // 把候选者信息发送给对方
    signalingServer.send(JSON.stringify({
      type: 'candidate',
      candidate: event.candidate
    }));
  } else {
    // 候选者收集完成
    console.log('所有ICE候选者已收集完毕');
  }
};

注意这个event.candidate为null的情况——它表示收集完成了。我刚开始写的时候没处理这个,结果一直收不到完成信号,排查了好久才发现。

接收对方的候选者

收到对方发来的候选者后,需要添加到RTCPeerConnection中:

signalingServer.onmessage = (event) => {
  const data = JSON.parse(event.data);
  
  if (data.type === 'candidate') {
    const candidate = new RTCIceCandidate(data.candidate);
    pc.addIceCandidate(candidate)
      .then(() => {
        console.log('成功添加ICE候选者');
      })
      .catch(err => {
        console.error('添加ICE候选者失败:', err);
      });
  }
};

注意事项:添加ICE候选者必须在setRemoteDescription之后进行。如果顺序反了,WebRTC会报错。另外,如果对方发来的候选者已经过期或者不匹配,addIceCandidate也会失败。

完整的SDP交换流程

我把整个流程画了一张图,方便你理解:

SDP交换与ICE候选者收集流程 发起方 (Offerer) 接收方 (Answerer) 1. 创建Offer 发送Offer (信令通道) 2. 设置远程描述 3. 创建Answer 发送Answer (信令通道) 4. 设置远程描述 5. 收集ICE候选者 5. 收集ICE候选者 交换ICE候选者 ✅ P2P连接建立成功

从图上可以看出来,整个流程是双向的。发起方先出手,接收方回应,然后双方同时收集ICE候选者并交换。等所有候选者都交换完毕,WebRTC就会自动选择最优路径建立连接。

实际项目中的注意事项

我在多个项目中总结了几点经验,分享给你:

  • 信令通道要可靠:SDP和ICE候选者都是通过信令通道传输的,如果信令通道不稳定,整个连接过程就会失败。我建议用WebSocket,别用HTTP轮询。
  • ICE重启:如果网络环境发生变化(比如从WiFi切到4G),可以调用pc.restartIce()重新收集候选者。
  • 超时处理:ICE收集不是无限等待的,一般设置一个30秒的超时。如果超时还没连上,就提示用户检查网络。
  • 日志记录:开发阶段一定要把SDP和ICE候选者的内容打印出来,方便排查问题。我曾经靠这个找到了一个编解码器不匹配的bug。

调试技巧:Chrome浏览器里输入chrome://webrtc-internals,可以看到所有WebRTC连接的详细信息,包括SDP内容、ICE状态、网络延迟等。这个工具救过我很多次。

好了,SDP交换和ICE候选者收集就讲到这里。记住一句话:SDP是“谈什么”,ICE是“怎么谈”。两者配合好了,P2P连接才能顺利建立。

公众号:蓝海资料掘金营,微信deep3321