当前位置: 首页 > news >正文

【音视频】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

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

var jsonMsg = {'cmd': 'join','roomId': roomId,'uid': localUserId,
};

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

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

jsonMsg = {'cmd': 'resp‐join','remoteUid': remoteUid
};

2.3 告知在房间里的人 new_peer

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

var jsonMsg = {'cmd': 'new‐peer','remoteUid': uid
};

2.4 主动离开房间 leave

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

var jsonMsg = {'cmd': 'leave','roomId': roomId,'uid': localUserId,
};

2.5 告知在房间里面的人 peer_leave

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

var jsonMsg = {'cmd': 'peer‐leave','remoteUid': uid
};

2.6 转发 offer

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

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

2.7 转发 answer

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

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

2.8 转发 candidate

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

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

var options = {offerToReceiveAudio: true,  // 是否希望接收音频,默认trueofferToReceiveVideo: true,  // 是否希望接收视频,默认trueiceRestart: 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 实例中,从而完成网络路径的验证与选择。

基本格式

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 格式)可直接用于构造对象:
// 从信令服务器接收的候选者数据(示例)
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 类型):
// 从信令服务器接收的候选者数据(示例)
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端示例

<!-- 用于显示远端视频的元素 -->
<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 网络连接、媒体流传输等全过程,构造函数可通过配置参数自定义连接行为。

基本语法
pc = new RTCPeerConnection([configuration]);
  • 参数 configuration:可选配置对象,用于自定义连接策略(如媒体传输方式、ICE 服务器等),缺省时使用浏览器默认配置。
核心配置项(configuration
配置项含义与可选值常用设置
bundlePolicy定义媒体轨(音频/视频)的传输通道绑定策略,影响传输效率(减少端口占用)。
- balanced:音频和视频轨使用各自的传输通道;
- max-compat:每个媒体轨单独使用一个传输通道(兼容性优先,效率低);
- max-bundle:所有媒体轨绑定到同一个传输通道(效率最高,现代浏览器推荐)。
max-bundle
iceTransportPolicy指定 ICE 候选者的筛选策略,控制网络路径选择。
- relay:仅使用中继候选者(依赖 TURN 服务器,适合严格防火墙环境);
- all:允许使用所有类型的候选者(包括本地局域网、公网直连、中继,兼容性最好)。
all
iceServersRTCIceServer 对象组成的数组,指定 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
配置示例
// 典型的 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 网络协商。

  • 示例

    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 数组(若对端关联了流)。
  • 示例

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() 后)。
  • 示例

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 连接已关闭。
  • 示例

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

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

http://www.lryc.cn/news/610021.html

相关文章:

  • SpringMVC在前后端分离架构中的执行流程详解
  • AI绘画-Stable Diffusion-WebUI的ControlNet用法
  • STM32F103C8T6 BC20模块NBIOT GPS北斗模块采集温湿度和经纬度发送到EMQX
  • 攻防世界-easyphp-lever1
  • k8s常见问题
  • 【ECCV2024】AdaCLIP:基于混合可学习提示适配 CLIP 的零样本异常检测
  • Design Compiler:高层次优化与数据通路优化
  • 【Spring Boot 快速入门】六、配置文件
  • Java 发送 HTTP POST请求教程
  • Scikit-learn - 机器学习库初步了解
  • MoonBit Pearls Vol.04:用MoonBit 探索协同式编程
  • Spring IoC容器与Bean管理
  • GPTs——定制的小型智能体
  • 白杨SEO:百度搜索开放平台发布AI计划是什么?MCP网站红利来了?顺带说说其它
  • [Oracle] || 连接运算符
  • 关于如何自定义vscode(wsl连接linux)终端路径文件夹文件名字颜色的步骤:
  • 【PHP】获取图片的主要颜色值RGB值
  • 【Django】-3- 处理HTTP响应
  • Django 性能优化详解:从数据库到缓存,打造高效 Web 应用
  • CNN卷积神经网络之MobileNet和ResNet(五)
  • AWS Lambda Function 全解:无服务器计算
  • CAD格式转换器HOOPS Exchange:全方位支持HOOPS系列产品
  • Webpack 搭建 Vue3 脚手架详细步骤
  • Baumer工业相机堡盟工业相机如何通过YoloV8深度学习模型实现人脸面部表情的追踪识别(C#代码UI界面版)
  • [3D数据存储] Archive (File Container) | 创建/写入/读取 | 存储格式HDF5
  • pyqt5-tools/pyqt6-tools 安装失败,解决办法
  • app-1
  • Spring P1 | 创建你的第一个Spring MVC项目(IDEA图文详解版,社区版专业版都有~)
  • 理解 Agent 的基本概念与功能
  • 正点原子STM32MP257开发板移植ubuntu24.04根文件系统(带桌面版)