在 Mediasoup 中,WebRtcTransport 的创建流程是一个涉及客户端与服务器端信令交互、资源初始化的关键过程,它是建立 WebRTC 媒体传输通道的基础。其创建流程可以概括为:客户端发起请求 -> 服务器端(Node.js 层)接收并处理 -> 转发至 C++ Worker 进程创建实际传输实例 -> 返回连接信息给客户端 。以下将详细拆解每个步骤,并提供核心代码示例。
一、 创建流程详细步骤
整个流程是信令驱动的,下图概述了从客户端调用 createWebRtcTransport 到获得 transportOptions 的主要步骤序列:
sequenceDiagram participant C as 客户端 participant N as Node.js服务器 participant W as C++ Worker进程 C->>N: 发送信令: createWebRtcTransport Note over N: 1. 验证权限与参数 N->>W: 调用worker.createWebRtcTransport() Note over W: 2. 创建内部Transport对象<br>3. 生成ICE参数及DTLS证书 W->>N: 返回内部Transport信息 Note over N: 4. 构建client所需的transportOptions N->>C: 响应transportOptions<br>(含id, iceParameters, dtlsParameters等)
步骤 1:客户端发起创建请求
客户端(通常基于 mediasoup-client 库)通过信令通道(WebSocket)向服务器发送一个信令请求,请求创建一个新的 WebRtcTransport。这个请求通常包含一个 forceTcp 标志(指示是否强制使用 TCP 候选者)等可选参数 。
步骤 2:Node.js 服务器层处理
Node.js 应用层(如 mediasoup-demo 中的 room.js)收到 createWebRtcTransport 信令。
- 权限与状态校验:检查用户是否在房间内,是否已存在 transport 等 。
- 参数准备 :组装创建
WebRtcTransport所需的选项 (options),主要包括:listenIps: 指定服务器监听的 IP 地址列表,用于生成 ICE 候选者。enableUdp/enableTcp: 是否启用 UDP/TCP 传输。preferUdp/preferTcp: 候选者优先级偏好。initialAvailableOutgoingBitrate: 初始可用出口带宽。appData: 自定义应用数据 。
- 调用 Worker :通过
worker.createWebRtcTransport(options)方法,将创建请求下发到对应的 C++ Worker 子进程 。
步骤 3:C++ Worker 进程创建核心对象
这是最核心的一步,发生在 mediasoup 的 C++ 子进程中。
- 创建
WebRtcTransport实例 :Worker 进程根据传入的options,实例化一个WebRtcTransport对象。该对象内部会管理 ICE、DTLS、SRTP 等协议栈 。 - 生成 ICE 参数 :
- 根据
listenIps,通过libnice或libwebrtc的 ICE 栈收集主机、服务器反射(如果配置了 STUN 服务器)和中继(如果配置了 TURN 服务器)候选者。 - 生成
iceParameters(包括usernameFragment和password),用于 ICE 连通性检查 。
- 根据
- 生成 DTLS 参数 :
- 创建或复用 DTLS 证书和指纹 (
dtlsParameters),用于后续的 DTLS 握手,建立加密通道 。
- 创建或复用 DTLS 证书和指纹 (
- 初始化内部状态 :初始化
RtpSender、RtpReceiver等组件,准备处理 RTP/RTCP 数据流 。
步骤 4:信息返回与客户端配置
- 构造响应 :C++ Worker 将创建好的
WebRtcTransport的内部信息(ID、ICE 参数、DTLS 参数、ICE 候选者列表等)返回给 Node.js 层。 - Node.js 层封装 :Node.js 层接收这些信息,并封装成一个
transportOptions对象,准备发送给客户端。 - 客户端接收与配置 :客户端收到
transportOptions后,使用这些参数配置本地的RTCPeerConnection,并通过addIceCandidate()添加 ICE 候选者,从而启动 ICE 连接建立过程 。
二、 核心代码示例
以下代码示例基于 mediasoup-demo 的常见模式,展示了服务器端(Node.js)和客户端的关键代码片段。
1. 服务器端 Node.js 代码示例
javascript
// 文件:server/room.js (示例)
const mediasoup = require('mediasoup');
async function createWebRtcTransport(socket, roomId, forceTcp = false) {
const room = rooms.get(roomId);
const consumerPeer = room.getPeer(socket.id);
if (!consumerPeer) {
throw new Error(`Peer ${socket.id} not found in room ${roomId}`);
}
// 步骤 2: 准备 WebRtcTransport 配置选项
const transportOptions = {
listenIps: [
{
ip: '0.0.0.0', // 监听所有 IPv4 地址
announcedIp: '192.168.1.100' // 对外宣布的 IP(如果是公网 IP 或域名)
}
],
enableUdp: !forceTcp, // 是否启用 UDP
enableTcp: true, // 启用 TCP 作为备选
preferUdp: true, // 优先使用 UDP
preferTcp: false, // TCP 优先级较低
initialAvailableOutgoingBitrate: 300000, // 初始出口带宽 300kbps
appData: { peerId: socket.id } // 自定义数据
};
// 步骤 2 & 4: 调用 Worker 创建 Transport 并获取结果
const transport = await room.worker.createWebRtcTransport(transportOptions); //
// 将 transport 与 peer 关联存储
consumerPeer.transports.set(transport.id, transport);
// 监听 transport 的 'icestatechange' 和 'dtlsstatechange' 事件以监控连接状态
transport.on('icestatechange', (iceState) => {
console.log(`Transport ${transport.id} ICE state changed to: ${iceState}`);
});
transport.on('dtlsstatechange', (dtlsState) => {
console.log(`Transport ${transport.id} DTLS state changed to: ${dtlsState}`);
if (dtlsState === 'connected') {
console.log(`DTLS handshake completed for transport ${transport.id}`);
}
});
// 步骤 4: 构建给客户端的响应数据
const response = {
id: transport.id,
iceParameters: transport.iceParameters, // ICE 用户名片段和密码
iceCandidates: transport.iceCandidates, // ICE 候选者列表
dtlsParameters: transport.dtlsParameters, // DTLS 指纹和角色
sctpParameters: transport.sctpParameters // 如果启用 SCTP(用于 DataChannel)
};
return response; // 此 response 将通过信令发送给客户端
}
// 信令处理部分(如使用 socket.io)
socket.on('createWebRtcTransport', async ({ forceTcp }, callback) => {
try {
const transportOptions = await createWebRtcTransport(socket, roomId, forceTcp);
callback({ transportOptions }); // 将 transportOptions 发送回客户端
} catch (error) {
callback({ error: error.message });
}
});
2. 客户端 JavaScript 代码示例
javascript
// 文件:client.js (示例,使用 mediasoup-client)
import * as mediasoupClient from 'mediasoup-client';
let device = null;
let sendTransport = null;
// 1. 加载设备(Device)
async function loadDevice(routerRtpCapabilities) {
device = new mediasoupClient.Device();
await device.load({ routerRtpCapabilities }); // 加载服务器的 RTP 能力
}
// 2. 创建发送 Transport(基于服务器返回的 transportOptions)
async function createSendTransport(transportOptionsFromServer) {
// transportOptionsFromServer 即服务器端 `createWebRtcTransport` 返回的对象
sendTransport = device.createSendTransport(transportOptionsFromServer); //
// 监听 Transport 事件,这些是必须实现的回调函数
sendTransport.on('connect', async ({ dtlsParameters }, callback, errback) => {
// 当 Transport 需要连接时(即启动 DTLS 握手),通知服务器
try {
// 通过信令发送 'connectWebRtcTransport' 请求,携带 dtlsParameters
await socket.emitWithAck('connectWebRtcTransport', {
transportId: sendTransport.id,
dtlsParameters
});
callback(); // 连接成功,通知底层库
} catch (error) {
errback(error); // 连接失败,通知底层库
}
});
sendTransport.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => {
// 当调用 transport.produce() 开始发送媒体时触发
try {
// 通过信令通知服务器创建一个新的 Producer
const { id } = await socket.emitWithAck('produce', {
transportId: sendTransport.id,
kind,
rtpParameters,
appData
});
callback({ id }); // 将服务器生成的 Producer ID 返回给底层库
} catch (error) {
errback(error);
}
});
sendTransport.on('connectionstatechange', (state) => {
console.log(`Send Transport connection state: ${state}`);
});
return sendTransport;
}
// 3. 主流程:接收服务器信令并创建 Transport
socket.on('newTransportOptions', async ({ transportOptions }) => {
// 假设服务器通过 'newTransportOptions' 事件下发 transportOptions
await createSendTransport(transportOptions);
console.log('Send WebRtcTransport created successfully.');
});
// 触发流程:首先加载设备,然后请求创建 Transport
async function init() {
// 从服务器获取 routerRtpCapabilities
const routerRtpCapabilities = await getRouterRtpCapabilitiesFromServer();
await loadDevice(routerRtpCapabilities);
// 发送信令请求创建 WebRtcTransport
socket.emit('createWebRtcTransport', { forceTcp: false }, (response) => {
if (response.error) {
console.error('Failed to create transport:', response.error);
return;
}
// 客户端收到 transportOptions,触发上面的 `newTransportOptions` 监听器
socket.emit('newTransportOptions', { transportOptions: response.transportOptions });
});
}
三、 关键点与内部机制
- ICE 候选者收集与优先级 :
listenIps配置直接影响候选者类型。announcedIp在服务器位于 NAT 后或需要暴露公网 IP 时至关重要。候选者优先级计算遵循 RFC 5245,通常host>srflx(server-reflexive) >relay。 - DTLS 角色协商 :在
connect阶段,客户端需要将本地的dtlsParameters(包含指纹和支持的密码套件)发送给服务器,服务器端的transport.connect()方法会完成 DTLS 角色(client/server)的最终确定和握手 。 - 多进程模型:创建操作最终在 C++ Worker 进程执行,这隔离了 CPU 密集型的媒体处理任务,提升了 Node.js 主进程的 I/O 响应能力 。
- 资源管理 :每个
WebRtcTransport都关联着底层网络套接字和协议状态机。在transport.close()被调用时,这些资源会被妥善释放 。
总结 :WebRtcTransport 的创建流程是 Mediasoup 信令交互的核心之一,它搭建了 ICE 和 DTLS 通道的骨架。客户端与服务器通过交换 iceParameters、iceCandidates 和 dtlsParameters 协同工作,为后续的 produce(发送媒体)和 consume(接收媒体)奠定安全的传输基础 。理解此流程对于调试连接问题(如 ICE 失败、DTLS 握手失败)至关重要。