【音视频】WebRTC 一对一通话-实现概述

一、实现方案

1.1 Web端

Web端使用html+JavaScript实现,因为WebRTC提供了一套完整的js接口可以调用,我们只需要调用RTCPeerConnection类就可以实现音视频通话,Web端主要实现以下部分:

  1. 界面设计
  2. 信令交互
  3. 媒体协商
  4. 网络协商
  5. 音视频流处理

1.2 服务器端

1.2.1 信令服务器

信令服务器这边我们使用C++开发,与Web端使用WebSocket进行通讯,使用JSON封装消息,因此在服务端我们引入WebSocketppnlonlohmann两个开源库,简化项目的实现难度,服务端实现以下功能:

  1. 维护一个通话房间,保持一对一通话逻辑
  2. 转发信令、媒体协商、网络协商消息

1.2.2 stun+turn服务器

stun服务器主要用于P2P打洞,turn服务器用于在打洞失败后的中继服务器,我们这里使用的是开源的服务器coturn,内部集成了stunturn服务器

1.3 测试环境

Web端使用的是微软的Edge浏览器,两个服务器都搭建在虚拟机Ubuntu上,局域网内通讯

二、信令设计

信令采取JSON格式封装,主要实现下面8个信令

  1. join 加入房间

  2. resp_­join 当join房间后发现房间已经存在另一个人时则返回另一个人的uid;如果只有自己则不返回

  3. leave 离开房间,服务器收到leave信令则检查同一房间是否有其他人,如果有其他人则通知他有人离开

  4. new_­peer 服务器通知客户端有新人加入,收到new­peer则发起连接请求

  5. peer_­leave 服务器通知客户端有人离开

  6. offer 转发offer sdp

  7. answer 转发answer sdp

  8. candidate 转发candidate sdp

2.1 加入房间 join

当有人加入房间的时候发送向服务器发送这个信令

json 复制代码
var jsonMsg = {
	'cmd': 'join',
	'roomId': roomId,
	'uid': localUserId,
};

2.2 回复加入房间的人 resp-join

这个信令主要是服务器告知加入房间的那个人,房间里面另一个人的信息,如果房间里面只有自己,则不会发送:

json 复制代码
jsonMsg = {
	'cmd': 'resp‐join',
	'remoteUid': remoteUid
};

2.3 告知在房间里的人 new_peer

这个信令和resp-join不一样的是,它是告诉房间里面的人加入者的信息,即服务器发送加入者的信息给房间里面的人:

json 复制代码
var jsonMsg = {
	'cmd': 'new‐peer',
	'remoteUid': uid
};

2.4 主动离开房间 leave

主动离开房间的时候,向服务器发送leave信令,服务器告知另一个人他已经离开了,如果房间没人就不需要了:

json 复制代码
var jsonMsg = {
	'cmd': 'leave',
	'roomId': roomId,
	'uid': localUserId,
};

2.5 告知在房间里面的人 peer_leave

有人离开的时候,如果房间里面还有人,那么服务器就会发送这条信令到客户端,告知另一个人已经离开了:

json 复制代码
var jsonMsg = {
	'cmd': 'peer‐leave',
	'remoteUid': uid
};

2.6 转发 offer

这个信令是客户端发送offer到服务端,服务端转发给对端的:

json 复制代码
var jsonMsg = {
	'cmd': 'offer',
	'roomId': roomId,
	'uid': localUserId,
	'remoteUid':remoteUserId,
	'msg': JSON.stringify(sessionDescription)
};

2.7 转发 answer

offer类似,服务端收到offer,需要回复一个answer,这个也是由服务器进行转发的:

json 复制代码
var jsonMsg = {
	'cmd': 'answer',
	'roomId': roomId,
	'uid': localUserId,
	'remoteUid':remoteUserId,
	'msg': JSON.stringify(sessionDescription)
};

2.8 转发 candidate

candidate是进行网络协商,同样需要服务端转发,其中一端收集到candidate之后,会通过服务器转发到客户端,协商后会将candidate存在客户端

json 复制代码
var jsonMsg = {
	'cmd': 'candidate',
	'roomId': roomId,
	'uid': localUserId,
	'remoteUid':remoteUserId,
	'msg': JSON.stringify(candidateJson)
};

2.9 信令交互

完整的信令交互如下:

三、媒体协商

  • 首先,呼叫方创建 Offer 类型的 SDP 消息。创建完成后,调用 setLocalDescriptoin 方法将该 Offer 保存到本地 Local 域,然后通过信令将 Offer 发送给被呼叫方。

  • 被呼叫方收到 Offer 类型的 SDP 消息后,调用 setRemoteDescription 方法将 Offer 保存到它的 Remote 域。

  • 作为应答,被呼叫方要创建 Answer 类型的 SDP 消息,Answer 消息创建成功后,再调用 setLocalDescription方法将 Answer 类型的 SDP 消息保存到本地的 Local 域。最后,被呼叫方将 Answer 消息通过信令发送给呼叫方。至此,被呼叫方的工作就完部完成了。

  • 接下来是呼叫方的收尾工作,呼叫方收到 Answer 类型的消息后,调用 RTCPeerConnecton 对象的setRemoteDescription 方法,将 Answer 保存到它的 Remote 域。至此,整个媒体协商过程处理完毕。

  • 当通讯双方拿到彼此的 SDP 信息后,就可以进行媒体协商了。媒体协商的具体过程是在 WebRTC 内部实现的,我们就不去细讲了,你只需要记住本地的 SDP 和远端的 SDP 都设置好后,协商就算成功了。

createOffer

  • 作用:由发起方(通常是呼叫方)创建一个 SDP(会话描述协议)提议,用于描述本地媒体能力(如是否接收音视频等),以启动 WebRTC 连接协商。

  • 基本格式
    aPromise = myPeerConnection.createOffer([options]);

可选参数options

js 复制代码
var options = {
  offerToReceiveAudio: true,  // 是否希望接收音频,默认true
  offerToReceiveVideo: true,  // 是否希望接收视频,默认true
  iceRestart: false           // 是否在连接活跃时重启ICE网络协商(仅活跃状态下false有效)
};

createAnswer

  • 作用 :由接收方(通常是被叫方)根据收到的Offer创建应答 SDP,回应自身的媒体能力,完成协商过程。

  • 基本格式
    aPromise = RTCPeerConnection.createAnswer([options]);

  • 注意 :目前options参数无效,无需传入配置。

setLocalDescription

  • 作用 :设置本地的会话描述(本地生成的OfferAnswer),使本地 PeerConnection 知晓自身的媒体配置。

  • 基本格式
    aPromise = RTCPeerConnection.setLocalDescription(sessionDescription);

    sessionDescriptioncreateOffercreateAnswer返回的 SDP 对象)

setRemoteDescription

  • 作用 :设置远程的会话描述(对方发送的OfferAnswer),使本地 PeerConnection 知晓对方的媒体配置,从而完成双方能力匹配。

  • 基本格式
    aPromise = pc.setRemoteDescription(sessionDescription);

    sessionDescription为对方通过信令服务器传递的 SDP 对象)

addTrack

  • 作用:向 PeerConnection 中添加本地媒体轨(音频 / 视频),使该媒体流能被传输到对端。

  • 基本格式
    rtpSender = rtcPeerConnection.addTrack(track, stream...);

  • 参数说明

    • track:要添加的媒体轨(如getUserMedia获取的音频轨audioTrack或视频轨videoTrack)。
    • stream:媒体轨所属的流(通常是getUserMedia返回的MediaStream对象)。
  • 返回值rtpSender对象,用于控制媒体轨的发送过程。

四、网络协商

主要是使用addIceCandidate方法:

  • addIceCandidate 是 WebRTC 中用于完成 ICE(Interactive Connectivity Establishment,交互式连接建立) 网络协商的核心方法。ICE 的作用是在复杂网络环境(如 NAT、防火墙后)中,帮助两个端点(Peer)发现彼此可达的网络路径(IP 地址 + 端口),最终建立直接的媒体传输通道。

  • 当本地或远端生成 ICE 候选者(Candidate)时,需要通过信令服务器将候选者传递给对方,对方再通过 addIceCandidate 方法将其添加到本地的 RTCPeerConnection 实例中,从而完成网络路径的验证与选择。

基本格式

js 复制代码
aPromise = pc.addIceCandidate(candidate);
  • 返回值:一个 Promise 对象,用于处理添加候选者的成功或失败(如候选者无效、超时等)。
  • 参数 candidate:ICE 候选者对象,包含网络路径的关键信息。

candidate 对象的属性

ICE 候选者由底层网络栈生成,包含以下关键属性(不同平台可能有细微差异):

属性 说明
candidate 候选者的核心描述字符串,包含传输协议(如 UDP/TCP)、IP 地址、端口、优先级等信息(格式遵循 SDP 规范)。
sdpMid 媒体流标识标签,与 SDP 中 m= 行的媒体流(音频 / 视频)关联,用于区分候选者属于哪路媒体。
sdpMLineIndex 数字索引,对应 SDP 中 m= 行的位置(如 0 表示第一路媒体,通常是音频;1 表示第二路,通常是视频),用于精准匹配媒体类型。
usernameFragment (可选)远端的唯一标识片段(ufrag),用于验证候选者的来源合法性,避免接收无效或恶意的候选者。

Android 与 Web 端的差异

ICE 候选者的处理逻辑在本质上一致,但由于平台 API 设计不同,存在以下细节差异:

Web 端(浏览器)

  • 浏览器会自动生成 RTCIceCandidate 对象,通过信令服务器接收的候选者数据(通常是 JSON 格式)可直接用于构造对象:
javascript 复制代码
// 从信令服务器接收的候选者数据(示例)
const candidateData = {
  candidate: "candidate:1 1 UDP 2130706431 192.168.1.100 5678 typ host",
  sdpMid: "audio",
  sdpMLineIndex: 0
};
// 构造 ICE 候选者并添加
const iceCandidate = new RTCIceCandidate(candidateData);
pc.addIceCandidate(iceCandidate)
  .then(() => console.log("候选者添加成功"))
  .catch(err => console.error("添加失败:", err));

Android 端(WebRTC 原生 API)

  • 需要通过 IceCandidate 类手动构造对象,注意参数类型匹配(如 sdpMLineIndex 为 int 类型):
java 复制代码
// 从信令服务器接收的候选者数据(示例)
String candidateStr = "candidate:1 1 UDP 2130706431 192.168.1.100 5678 typ host";
String sdpMid = "audio";
int sdpMLineIndex = 0;

// 构造 IceCandidate 对象
IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, candidateStr);
// 添加到 PeerConnection
peerConnection.addIceCandidate(iceCandidate);

注意:Android 端的 addIceCandidate 方法没有返回 Promise,而是通过 PeerConnection.Observer 的回调(如 onIceCandidateError)处理结果。

显示远端媒体流

ICE 协商完成、媒体通道建立后,远端的音视频流会通过 RTCPeerConnection 传输到本地。本地需要监听流事件,获取远端媒体轨(Track),并通过 HTML 元素(如 <video><audio>)显示或播放。

  1. 监听媒体轨事件

    远端媒体流传输时,本地 RTCPeerConnection 会触发 track 事件(替代了旧版的 addstream 事件,addstream 已废弃),事件中包含远端的媒体轨(MediaStreamTrack)。

  2. 关联媒体流与元素

    将获取到的媒体轨添加到 MediaStream 中,再将流绑定到 <video><audio> 元素的 srcObject 属性,即可显示或播放。

Web端示例

html 复制代码
<!-- 用于显示远端视频的元素 -->
<video id="remoteVideo" autoplay playsinline></video>

<script>
const pc = new RTCPeerConnection(config);
const remoteVideo = document.getElementById('remoteVideo');

// 监听远端媒体轨事件
pc.ontrack = (event) => {
  // event.streams 可能包含远端的 MediaStream(若对方通过 addTrack 关联了流)
  const remoteStream = event.streams[0];
  
  if (remoteStream) {
    // 将远端流绑定到 video 元素
    remoteVideo.srcObject = remoteStream;
  } else {
    // 若没有关联流,手动创建流并添加轨道
    const newStream = new MediaStream();
    newStream.addTrack(event.track);
    remoteVideo.srcObject = newStream;
  }
};
</script>

五、RTCPeerConnection

构造函数

RTCPeerConnection 是 WebRTC 端到端连接的核心对象,用于管理媒体协商、ICE 网络连接、媒体流传输等全过程,构造函数可通过配置参数自定义连接行为。

基本语法
javascript 复制代码
pc = new RTCPeerConnection([configuration]);
  • 参数 configuration:可选配置对象,用于自定义连接策略(如媒体传输方式、ICE 服务器等),缺省时使用浏览器默认配置。
核心配置项(configuration
配置项 含义与可选值 常用设置
bundlePolicy 定义媒体轨(音频/视频)的传输通道绑定策略,影响传输效率(减少端口占用)。 - balanced:音频和视频轨使用各自的传输通道; - max-compat:每个媒体轨单独使用一个传输通道(兼容性优先,效率低); - max-bundle:所有媒体轨绑定到同一个传输通道(效率最高,现代浏览器推荐)。 max-bundle
iceTransportPolicy 指定 ICE 候选者的筛选策略,控制网络路径选择。 - relay:仅使用中继候选者(依赖 TURN 服务器,适合严格防火墙环境); - all:允许使用所有类型的候选者(包括本地局域网、公网直连、中继,兼容性最好)。 all
iceServers RTCIceServer 对象组成的数组,指定 ICE 代理服务器(STUN/TURN),用于穿透 NAT、防火墙。 每个 RTCIceServer 包含以下属性: - urls:服务器地址数组(如 stun:stun.example.orgturn:turn.example.org:3478); - username:TURN 服务器的认证用户名(STUN 无需); - credential:TURN 服务器的认证凭据(STUN 无需); - credentialType:凭据类型("password""oauth",默认 password)。 示例: [{ urls: "stun:stun.l.google.com:19302" }](公共 STUN 服务器)
rtcpMuxPolicy 定义 RTCP(实时传输控制协议)与 RTP(实时传输协议)的复用策略(RTCP 用于监控媒体传输质量)。 - negotiate:尝试复用,若无法复用则分开传输; - require:强制复用,若无法复用则连接失败(减少端口占用,推荐)。 require
配置示例
javascript 复制代码
// 典型的 RTCPeerConnection 配置
const configuration = {
  bundlePolicy: "max-bundle",
  iceTransportPolicy: "all",
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" }, // 公共 STUN 服务器(用于获取公网地址)
    { 
      urls: "turn:turn.example.com:3478", 
      username: "user123", 
      credential: "pass456" 
    } // TURN 服务器(用于中继,当直连失败时)
  ],
  rtcpMuxPolicy: "require"
};

const pc = new RTCPeerConnection(configuration);
重要事件

RTCPeerConnection 通过事件机制反馈连接状态、媒体流变化等关键信息,是实现实时通信逻辑的核心依据。

1. onicecandidate
  • 触发时机 :本地 ICE 框架生成新的 ICE 候选者(网络路径信息)时触发;当所有候选者收集完成后,会触发一次 candidatenull 的事件。

  • 作用:获取本地候选者并通过信令服务器发送给对端,用于完成 ICE 网络协商。

  • 示例

    javascript 复制代码
    pc.onicecandidate = (event) => {
      if (event.candidate) {
        // 发送候选者给对端(通过信令服务器)
        signalingServer.send({ type: "candidate", candidate: event.candidate });
      } else {
        // 所有候选者收集完成
        console.log("ICE 候选者收集完毕");
      }
    };
2. ontrack
  • 触发时机 :远端通过 addTrack 添加的媒体轨(音频/视频)传输到本地时触发(替代了旧版的 onaddstream 事件,onaddstream 已废弃)。

  • 作用:获取远端媒体轨,用于显示或播放远端音视频。

  • 事件对象属性

    • track:远端媒体轨(MediaStreamTrack 实例,如音频轨 AudioTrack 或视频轨 VideoTrack);
    • streams:远端媒体轨所属的 MediaStream 数组(若对端关联了流)。
  • 示例

javascript 复制代码
const remoteVideo = document.getElementById("remoteVideo");

pc.ontrack = (event) => {
const [remoteStream] = event.streams;
// 将远端流绑定到 video 元素播放
if (remoteStream) {
  remoteVideo.srcObject = remoteStream;
} else {
  // 若对端未关联流,手动创建流并添加轨道
  const newStream = new MediaStream();
  newStream.addTrack(event.track);
  remoteVideo.srcObject = newStream;
}
};
3. onconnectionstatechange
  • 触发时机RTCPeerConnection 的整体连接状态变化时触发(反映端到端连接的最终状态)。

  • 状态值(pc.connectionState

    • new:初始状态,连接尚未建立;
    • connecting:正在建立连接(SDP 协商或 ICE 连接中);
    • connected:连接已完全建立,媒体可正常传输;
    • disconnected:连接中断(如网络临时故障,可能恢复);
    • failed:连接失败(无法恢复,需重新创建连接);
    • closed:连接已被主动关闭(调用 pc.close() 后)。
  • 示例

javascript 复制代码
pc.onconnectionstatechange = (event) => {
	switch (pc.connectionState) {
	  case "connected":
		console.log("连接已建立,可传输媒体");
		break;
	  case "disconnected":
		console.warn("连接中断,尝试重连...");
		break;
	  case "failed":
		console.error("连接失败,需重新初始化");
		// 处理失败逻辑(如重新创建 RTCPeerConnection)
		break;
	  case "closed":
		console.log("连接已关闭");
		break;
	}
};
4. oniceconnectionstatechange
  • 触发时机 :ICE 连接状态变化时触发(专注于网络层连接状态,比 connectionState 更细致)。

  • 状态值(pc.iceConnectionState

    • new:ICE 开始初始化;
    • checking:正在检查本地与远端的候选者,尝试建立连接;
    • connected:ICE 已找到可用路径,媒体开始传输(但可能仍在收集更多候选者);
    • completed:ICE 候选者收集完成,且已确定最佳路径;
    • failed:ICE 无法找到可用路径(需检查 ICE 服务器配置);
    • disconnected:ICE 连接中断(如网络变化);
    • closed:ICE 连接已关闭。
  • 示例

javascript 复制代码
pc.oniceconnectionstatechange = (event) => {
	console.log("ICE 连接状态:", pc.iceConnectionState);
	if (pc.iceConnectionState === "failed") {
	  console.error("ICE 连接失败,可能需要重启 ICE 协商");
	  // 可尝试调用 pc.restartIce() 重启协商
	}
};

更多资料:https://github.com/0voice

相关推荐
@BreCaspian7 小时前
Kazam产生.movie.mux后恢复视频为.mp4
linux·ubuntu·音视频
AI technophile8 小时前
OpenCV计算机视觉实战(18)——视频处理详解
opencv·计算机视觉·音视频
伏 念9 小时前
音视频学习笔记
笔记·音视频
Antonio91510 小时前
【音视频】WebRTC 中的RTP、RTCP、SDP、Candidate
音视频·webrtc
且随疾风前行.13 小时前
在安卓中使用 FFmpegKit 剪切视频并添加文字水印
android·音视频
开开心心就好1 天前
专业鼠标点击器,自定义间隔次数
javascript·安全·计算机外设·excel·音视频·模拟退火算法
却道天凉_好个秋1 天前
音视频学习(四十七):模数转换
音视频
DogDaoDao1 天前
WebRTC前处理模块技术详解:音频3A处理与视频优化实践
音视频·webrtc·实时音视频·视频增强·视频前处理·3a算法·音频前处理
却道天凉_好个秋1 天前
音视频学习(五十):音频无损压缩
音视频·无损压缩