2、创建Offer:RTCPeerConnection对象创建、createOffer方法详解、Offer选项

好,咱们正式开始走媒体协商的第一步——创建Offer。

这一步说白了,就是让浏览器说一句:“我想跟你建立连接,我支持这些媒体能力,你看看行不行?”

嗯,听起来简单,但里面坑不少。我刚开始做WebRTC的时候,就因为在创建Offer时少传了一个参数,排查了整整一个下午。咱们今天就把这块彻底讲透。

2.1 RTCPeerConnection对象创建

创建Offer之前,你得先有一个RTCPeerConnection对象。这个对象是整个WebRTC连接的核心,所有媒体协商、数据传输都靠它来调度。

创建方式很简单,直接new一个就行:

const pc = new RTCPeerConnection();

但实际项目中,我几乎不会这么裸写。为什么呢?因为你需要配置STUN/TURN服务器。否则在真实网络环境下,两个不同内网的设备根本连不上。

我个人习惯这样写:

const config = {
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    {
      urls: 'turn:your-turn-server.com:3478',
      username: 'your-username',
      credential: 'your-credential'
    }
  ]
};

const pc = new RTCPeerConnection(config);
小提示:STUN服务器是公开的,Google那个可以直接用。但TURN服务器需要自己部署或购买服务。我在项目中遇到过,如果不配TURN,某些对称NAT环境下连接会一直卡在“connecting”状态。

创建完RTCPeerConnection之后,你还需要做一件事——添加媒体轨道。因为Offer里要包含你打算发送的媒体信息,浏览器得知道你要传音频还是视频。

// 获取本地媒体流
const stream = await navigator.mediaDevices.getUserMedia({
  audio: true,
  video: true
});

// 添加轨道到连接中
stream.getTracks().forEach(track => {
  pc.addTrack(track, stream);
});

嗯,这里要注意:addTrack必须在createOffer之前调用。否则生成的Offer里不会有任何媒体信息,对方收到后也不知道该跟你协商什么。

2.2 createOffer方法详解

有了RTCPeerConnection对象,接下来就可以调用createOffer了。

这个方法的作用,是让浏览器根据你添加的媒体轨道,生成一个SDP(Session Description Protocol)描述。这个描述里包含了你的媒体能力、编码格式、网络信息等。

基本用法:

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

createOffer返回的是一个RTCSessionDescription对象,里面主要包含两个字段:

字段 说明
type 固定为"offer"
sdp 实际的SDP文本,包含媒体描述

你可能会问:为什么还要调用setLocalDescription?

因为createOffer只是生成了一个提案,你得告诉浏览器:“就用这个提案作为我的本地描述。” 这样浏览器才能根据这个描述去收集ICE候选、准备传输。

注意:createOffer是一个异步操作,必须用await或.then来处理。我曾经见过有人忘了await,结果拿到的是一个Promise对象,后面所有逻辑都乱了。

2.3 Offer选项(offerToReceiveAudio/Video)

现在咱们聊聊Offer选项。createOffer可以接收一个参数——RTCOfferOptions对象。最常用的两个选项就是offerToReceiveAudio和offerToReceiveVideo。

这两个选项的作用是:告诉浏览器,你希望接收对方的音频或视频。即使你本地没有添加对应的媒体轨道,也可以设置这些选项。

举个例子:

const offerOptions = {
  offerToReceiveAudio: true,
  offerToReceiveVideo: false
};

const offer = await pc.createOffer(offerOptions);

这个场景很常见。比如你做一个音频通话应用,用户只开了麦克风,没开摄像头。但你仍然希望对方能给你发视频?那就可以把offerToReceiveVideo设为true。

我整理了一下这两个选项的常见组合:

场景 offerToReceiveAudio offerToReceiveVideo
纯音频通话 true false
音视频通话 true true
仅接收视频(无声) false true
仅发送,不接收 false false
核心要点:offerToReceiveAudio/Video控制的是“接收”方向的能力。你本地添加的轨道控制的是“发送”方向。两者是独立的,可以自由组合。

除了这两个,RTCOfferOptions还有其他选项,比如iceRestart、voiceActivityDetection等。但日常开发中,最常用的就是offerToReceiveAudio和offerToReceiveVideo。

2.4 完整流程示意图

咱们把整个创建Offer的流程画出来,方便你理解:

创建Offer完整流程 1. 创建RTCPeerConnection 配置STUN/TURN服务器 2. 添加媒体轨道 addTrack() 添加音视频 3. 调用createOffer 传入Offer选项参数 createOffer 返回 RTCSessionDescription 对象 包含 type: "offer" 和 sdp: 具体SDP文本 4. setLocalDescription(offer) 将Offer设置为本地描述,开始收集ICE候选 5. 通过信令通道发送Offer 将offer对象序列化后发送给远端 注:步骤1-3在本地完成,步骤5需要信令服务器配合

2.5 实际项目中的避坑指南

最后,分享几个我在项目中踩过的坑:

  • 不要重复创建Offer:如果连接已经建立,再调用createOffer会导致状态错误。建议先检查signalingState是否为"stable"。
  • Offer选项不是万能的:offerToReceiveAudio设为true,不代表对方一定会发音频。它只是告诉浏览器“我希望接收”,最终能不能收到,还得看对方是否发送。
  • setLocalDescription会触发ICE收集:调用之后,你会开始收到icecandidate事件。记得在创建Offer之前就注册好事件监听。
  • 我曾经犯过的错:有一次我在createOffer之后,直接就把offer字符串通过信令发给了对方,忘了调用setLocalDescription。结果对方回复Answer后,本地状态一直对不上,连接始终建立不起来。排查了半天才发现是这个问题。
我的建议:把创建Offer的流程封装成一个函数,每次调用前先重置状态。这样能避免很多因为状态混乱导致的问题。

嗯,创建Offer这部分就讲到这里。核心就是三步:建连接、加轨道、生成Offer。选项参数控制的是接收方向的能力,别搞混了就行。

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