小程序直播连麦不是「开个 live-pusher 就完事」的功能。本文以 ZEGO 实时音视频 SDK(ZEGO Express SDK) 为主线,从架构到代码逐层拆解一条完整的直播连麦链路:主播推流 → CDN 分发 → 观众上麦 → 云端混流 → 海量观看。
一、架构总览:一条流不够用的时候
先区分两种直播:
| 纯直播 | 直播连麦 | |
|---|---|---|
| 数据流向 | 主播 → 观众(单向) | 主播 ↔ 观众(双向) |
| 延迟 | CDN 分发 3-5s | RTC 端到端 < 200ms |
| 观众角色 | 只看 | 可以上麦变主播 |
| 技术重心 | CDN 分发 | RTC + 混流 + CDN |
连麦场景下,数据流变成三条链路:
主播端 ──RTC推流──→ ZEGO 云 ──CDN转推──→ 海量观众(普通观众只看)
│ │
│ ┌─ 混流服务 ─┐
│ │ │
上麦观众 ──RTC推流──┘ 混流输出 ──→ CDN ──→ 普通观众拉流
- RTC 链路:主播和连麦者之间的实时通道,延迟 ~200ms,各自推流 + 互拉对方的流
- 混流链路:ZEGO 云端将多路流合成一路,观众只需拉一条流
- CDN 链路:混流后的单路流分发到海量观众,撑住并发
注意:混流是规模化后的优化手段,不是连麦的前置条件。1v1 连麦时直接互拉 RTC 流即可,不需要混流。
一句话总结本质:RTC 负责互动,混流负责合一,CDN 负责分发。
二、技术选型:为什么 ZEGO + 小程序
2.1 微信原生的能力边界
微信小程序提供了 <live-pusher> 和 <live-player> 两个原生组件,能推能拉。但它俩本质上走的是 CDN 链路,延迟在秒级,且不支持多人实时互通,两个人各自推流、各自拉对方的流,靠 CDN 转发做不到毫秒级的实时互动。
所以需要 RTC SDK 来填补实时通道。ZEGO Express SDK 的工作方式是:SDK 通过 startPublishingStream 和 startPlayingStream 返回推拉流 URL,然后复用微信原生 <live-pusher> / <live-player> 组件渲染。这比自建 WebRTC 方案省掉了信令服务、ICE 穿透、SFU 部署全套基础设施。
2.2 需要哪些 ZEGO 产品
| 产品 | 用途 |
|---|---|
| Express SDK(实时音视频-小程序版) | RTC 推拉流 + 混流 + CDN 转推,核心 SDK |
| ZIM SDK(即时通讯-小程序版) | 可选:上麦信令、麦位管理、聊天消息 |
Express SDK 本身就提供了房间消息能力(sendBroadcastMessage / IMRecvBroadcastMessage),信令交互不复杂时不需要额外集成 ZIM。但当你要做完整的麦位管理(申请、同意、拒绝、踢人、锁麦),ZIM 的自定义消息会更灵活。
三、集成准备
3.1 控制台获取凭证
在 ZEGO 控制台 创建项目,拿到两个关键信息:
- AppID:应用标识,数字格式
- Server 地址:形如
wss://xxx.zego.im的 WebSocket 地址 - ServerSecret:服务端密钥,32 字节字符串,只放在服务端
3.2 小程序后台配置
在微信公众平台「开发管理 → 开发设置 → 服务器域名」中,将 ZEGO 相关域名加入白名单:
request合法域名:填 ZEGO 控制台提供的上报地址socket合法域名:填 ZEGO Server 地址(wss:// 去掉 wss://)
然后在「接口设置」中开启实时播放音视频流和实时录制音视频流两个权限开关。
3.3 基础库版本
微信小程序基础库 ≥ 1.7.0(低于此版本不支持 live-pusher / live-player)。
3.4 导入 SDK
从 ZEGO 官网下载 ZegoExpressMiniProgram-x.x.x.js,放到小程序项目目录中,在使用的页面 JS 文件中引入:
// 假设 SDK 放在 utils 目录下
import ZegoExpressEngine from '../../utils/ZegoExpressMiniProgram-x.x.x';
SDK 暴露一个构造函数 ZegoExpressEngine,后续所有操作都通过它的实例完成。
3.5 兼容性检测
SDK 提供了 checkSystemRequirements 方法,初始化后调用,建议作为首屏逻辑:
const zg = new ZegoExpressEngine(appID, server);
const result = await zg.checkSystemRequirements();
// result.code === 0 → 支持
// result.code === 10001 → 微信版本过低
// result.code === 10002 → 摄像头/麦克风权限未授权
四、Token 鉴权:服务端生成,客户端消费
所有进房间的用户都需要 Token。Token 必须由服务端生成,绝不能在前端拼接 ServerSecret。
4.1 服务端生成 Token(Node.js 示例)
ZEGO 使用 Token04 版本进行身份鉴权。核心逻辑是 AES-GCM 加密 token payload 后用 base64 编码。以下代码来自 ZEGO 官方 Server Assistant SDK:
import { createCipheriv, randomBytes } from 'crypto';
function generateToken04(appId, userId, secret, effectiveTimeInSeconds, payload = '') {
const VERSION_FLAG = '04';
const createTime = Math.floor(Date.now() / 1000);
const tokenInfo = {
app_id: appId,
user_id: userId,
nonce: makeNonce(),
ctime: createTime,
expire: createTime + effectiveTimeInSeconds,
payload: payload || ''
};
const plainText = JSON.stringify(tokenInfo);
const { encryptBuf, nonce } = aesGcmEncrypt(plainText, secret);
// 拼装二进制 token body:expire(8B) | nonceLen(2B) | nonce(12B) | dataLen(2B) | data | mode(1B)
const buf = packTokenBody(tokenInfo.expire, nonce, encryptBuf);
return VERSION_FLAG + Buffer.from(buf).toString('base64');
}
function makeNonce() {
const min = -(2 ** 31), max = 2 ** 31 - 1;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function aesGcmEncrypt(plainText, key) {
const nonce = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, nonce);
cipher.setAutoPadding(true);
const encrypted = cipher.update(plainText, 'utf8');
const encryptBuf = Buffer.concat([encrypted, cipher.final(), cipher.getAuthTag()]);
return { encryptBuf, nonce };
}
在你的 Web 框架里暴露一个 GET 端点:
// GET /api/zego/token?userId=xxx&effectiveTime=3600
app.get('/api/zego/token', (req, res) => {
const { userId, effectiveTime = 3600 } = req.query;
const token = generateToken04(
Number(process.env.ZEGO_APP_ID),
userId,
process.env.ZEGO_SERVER_SECRET,
Number(effectiveTime)
);
res.send(token);
});
4.2 客户端使用 Token
小程序端从自己的服务端获取 Token 后传入 loginRoom:
const token = await fetch('/api/zego/token?userId=' + userId).then(r => r.text());
await zg.loginRoom(roomID, token, {
userID: userId,
userName: nickName
}, {
userUpdate: true // 设为 true 才能收到 roomUserUpdate 回调
});
五、主播端:推流到 ZEGO 云
5.1 初始化引擎并注册回调
// 初始化
const zg = new ZegoExpressEngine(appID, server);
// 房间连接状态
zg.on('roomStateUpdate', (roomID, state, errorCode) => {
if (state === 'CONNECTED') {
// 可以开始推流了
} else if (state === 'DISCONNECTED') {
// 与房间断开,处理重连
}
});
// 房间内流变更通知 — 这是连麦流程的关键回调
zg.on('roomStreamUpdate', async (roomID, updateType, streamList) => {
if (updateType === 'ADD') {
// 有新流加入,开始拉流(观众端/主播端看到新上麦者)
for (const stream of streamList) {
const { url } = await zg.startPlayingStream(stream.streamID);
// 将拉流 URL 挂载到 live-player 渲染
}
} else if (updateType === 'DELETE') {
// 有流退出,停止拉流
for (const stream of streamList) {
await zg.stopPlayingStream(stream.streamID);
}
}
});
// 推流状态
zg.on('publisherStateUpdate', (result) => {
// result.state: "PUBLISHING" | "NO_PUBLISH" | "PUBLISH_REQUESTING"
});
5.2 登录并推流
// 1. 登录房间
await zg.loginRoom(roomID, token, {
userID: anchorUserId,
userName: '主播小明'
}, { userUpdate: true });
// 2. 开始推流,streamID 需在 AppID 下唯一
const pushStreamID = `anchor_${anchorUserId}_${Date.now()}`;
const { url } = await zg.startPublishingStream(pushStreamID);
// 3. 将推流 URL 绑定到页面数据,live-pusher 组件自动推流
this.setData({ livePusherUrl: url });
5.3 WXML 模板
<!-- 主播端视图:自己的画面 + 拉到的连麦者画面 -->
<view class="live-container">
<!-- 本地推流 -->
<live-pusher
wx:if="{{livePusherUrl}}"
url="{{livePusherUrl}}"
mode="SD"
autopush
min-bitrate="800"
max-bitrate="1500"
aspect="3:4"
bindstatechange="onPushStateChange"
bindnetstatus="onPushNetStateChange"
/>
<!-- 远端拉流列表(连麦者的画面) -->
<live-player
wx:for="{{livePlayerList}}"
wx:key="streamID"
id="{{item.streamID}}"
src="{{item.url}}"
mode="RTC"
autoplay
bindstatechange="onPlayStateChange"
bindnetstatus="onPlayNetStateChange"
/>
</view>
mode 参数的含义:
SD:普清延迟约 3-5s,CDN 模式RTC:实时通话模式,延迟 < 400ms,连麦场景必须用 RTC
六、观众端:拉流观看
普通观众不需要推流,只拉流观看。
6.1 进入房间并拉流
// 1. 登录同一房间
await zg.loginRoom(roomID, token, {
userID: viewerUserId,
userName: '观众小红'
}, { userUpdate: true });
// 2. 通过 roomStreamUpdate 回调获取存在的流并拉取
// (回调已在初始化时注册,这里处理拉流逻辑)
zg.on('roomStreamUpdate', async (roomID, updateType, streamList) => {
if (updateType === 'ADD') {
for (const stream of streamList) {
const { url } = await zg.startPlayingStream(stream.streamID);
// 追加到 livePlayerList
const newPlayer = { streamID: stream.streamID, url };
this.setData({
livePlayerList: [...this.data.livePlayerList, newPlayer]
});
}
}
});
6.2 大规模场景走 CDN
上面的方案是观众直接拉 RTC 流——小规模(几十人)没问题,但成百上千观众同时拉 RTC 流既不经济也没必要。规模化场景应该走 CDN:
// 主播端推流成功后,开启 CDN 转推
const targetURL = 'rtmp://推流域名/接入点/' + pushStreamID;
await zg.addPublishCdnUrl(pushStreamID, targetURL);
// 观众端通过 CDN 播放地址拉流
// 此时 mode 可以设为 "SD" 或者直接用 H5 播放器
更合理的做法是只把混流后的那一路输出到 CDN(见第七章),而不是把每路原始流都转推 CDN。
七、连麦核心流程:从观众到主播
这是全文的核心,观众如何上麦,成为内容生产者。
7.1 上麦信令流程
观众端 服务端/信令通道 主播端
│ │ │
│── ① 发送上麦申请 ──────→ │ │
│ │── ② 推送上麦申请 ─────────→ │
│ │ │
│ │←── ③ 主播同意 ──────────── │
│←── ④ 通知上麦成功 ──── │ │
│ │ │
│── ⑤ startPublishingStream │ │
│ (开始推自己的流) │ │
│ │ │
│ roomStreamUpdate 通知所有人有新增流 │
信令可以用 ZEGO 房间消息实现:
// 观众端:发送上麦申请
await zg.sendBroadcastMessage(roomID, JSON.stringify({
type: 'apply_cohost',
userId: viewerUserId,
userName: '观众小红'
}));
// 主播端:监听消息
zg.on('IMRecvBroadcastMessage', (roomID, msgList) => {
for (const msg of msgList) {
const data = JSON.parse(msg.message);
if (data.type === 'apply_cohost') {
// 弹窗询问主播是否同意
}
}
});
当信令逻辑变复杂(麦位管理、踢人、锁麦、邀请上麦),建议改用 ZIM 小程序 SDK,有更完善的自定义消息、会话管理和离线推送能力。
7.2 上麦者推流
// 上麦观众收到同意通知后,开始推自己的流
const cohostStreamID = `cohost_${userId}_${Date.now()}`;
const { url } = await zg.startPublishingStream(cohostStreamID);
this.setData({
livePusherUrl: url,
isCohosting: true
});
7.3 主播端处理新流
主播已经在初始化时注册了 roomStreamUpdate 回调,上麦者推流后主播会自动收到 ADD 通知,拉流渲染即可。不需要额外代码。
7.4 观众端感知连麦
观众同样通过 roomStreamUpdate 获知新流。如果直播间只有主播 + 1 个连麦者,观众直接多拉一路 <live-player> 即可。但如果连麦者人数多,或者观众量级大,就需要混流——每个终端同时拉 4 路以上 RTC 流就会有性能压力。
7.5 下麦
// 上麦者停止推流
await zg.stopPublishingStream(cohostStreamID);
// 其他端在 roomStreamUpdate 中收到 DELETE 后:
await zg.stopPlayingStream(cohostStreamID);
// 从 livePlayerList 中移除对应项
八、混流:多路合一路
8.1 连麦不依赖混流
首先要厘清一个概念:连麦和混流是两件事。
连麦的核心是 RTC 房间内多人互相推拉流,主播推自己的流,上麦者也推自己的流,彼此通过 startPlayingStream 互拉。这个过程不需要混流,直接 startPlayingStream 拉对方的 streamID 即可。1 个主播 + 1 个连麦者,每人拉对方 1 路流,体验完全 OK。
8.2 什么时候需要混流
混流不是连麦的前置条件,而是节目规模扩大后的优化手段。当 1 个主播 + 3 个连麦者 = 4 路 RTC 流,每个观众都拉 4 路流的问题就暴露了:
- 小程序性能:4 个
<live-player>同时解码渲染,中低端机型直接卡死 - 带宽成本:4 × 观众数 × 码率,成本翻 4 倍
- 弱网体验:4 路流抢带宽,哪路都看不清
混流的解法:云端把 N 路流合成 1 路,观众只拉一路。
8.2 手动混流实现
ZEGO 支持三种混流方式:手动混流(自定义输入流、布局、输出)、自动混流(指定房间自动混音频)、全自动混流(免开发)。连麦场景首选手动混流,因为需要自定义视频布局。
以下是 ZEGO 官方文档的混流示例代码:
// 构造输入流列表:主播流 + 连麦者流
const inputList = [{
streamID: anchorStreamID,
contentType: 'video',
layout: {
top: 0,
left: 0,
bottom: 480,
right: 640,
}
}];
// 遍历已拉到的连麦者流,加入混流输入
for (const player of this.data.livePlayerList) {
inputList.push({
streamID: player.streamID,
contentType: 'video',
layout: {
top: 480, // 连麦者画面放在下半部分
left: 0,
bottom: 960,
right: 640
}
});
}
// 输出配置
const mixParam = {
taskID: this.data.mixTaskID, // 混流任务 ID,需唯一
inputList: inputList,
outputList: [{ target: this.data.mixStreamID }],
outputConfig: {
outputBitrate: 800 * 1000, // 800 kbps
outputFPS: 15,
outputWidth: 640,
outputHeight: 960,
}
};
const result = await zg.startMixerTask(mixParam);
if (result.errorCode !== 0) {
console.error('混流失败', result);
return;
}
混流启动后,观众端拉混流输出:
// 拉混流,sourceType: "BGP" 表示走 ZEGO 自建网络
const { streamID, url } = await zg.startPlayingStream(
this.data.mixStreamID,
{ sourceType: 'BGP' }
);
this.setData({
mixPlayerUrl: url
});
8.3 连麦者变化时更新混流
有人上麦或下麦时,需要更新混流输入列表,重新调用 startMixerTask 更新混流配置即可:
// 有人下麦后重新构造 inputList,再次调用
await zg.startMixerTask(updatedMixParam);
8.4 停止混流
// 注意:开始混流和停止混流必须用同一个 taskID
const { errorCode } = await zg.stopMixerTask(this.data.mixTaskID);
// 停止拉混流
await zg.stopPlayingStream(this.data.mixStreamID);
⚠️ 混流任务持续计费。在直播结束后务必调用
stopMixerTask,否则任务会一直运行到房间关闭。
九、CDN 转推:从 RTC 到海量分发
混流输出本身还是一路 RTC 流,—如果数千观众同时拉这路流,需要把它推到 CDN:
// 将混流输出转推到 CDN
const cdnUrl = 'rtmp://你的推流域名/live/' + mixStreamID;
await zg.addPublishCdnUrl(mixStreamID, cdnUrl);
建议在生产环境使用服务端 API 控制 CDN 转推,而非客户端调用。原因是客户端 addPublishCdnUrl 只通知 ZEGO 服务器「尝试转推」,无法保证成功,且生命周期依赖客户端在线状态。
转推后,观众通过 CDN 播放地址拉流,此时 <live-player> 的 mode 可以切换为 SD,走 CDN 分发链路。Web 端观众可以直接用 H5 播放器播放。
CDN 推流鉴权
建议在 ZEGO 控制台开启 CDN 推流鉴权,防止他人伪造推流 URL 盗用带宽。开启后推流 URL 需要拼接鉴权参数。
十、延迟控制
连麦体验最敏感的指标不是画质,是延迟。
| 链路段 | 典型延迟 | 影响因素 |
|---|---|---|
| 推流端采集 + 编码 | ~20-50ms | 设备性能、编码参数 |
| RTC 端到端传输 | ~50-150ms | 网络 RTT、SDK QoS 策略 |
| 混流处理 | ~200-500ms | 输入流数量、输出配置 |
| CDN 分发 | ~1-3s | CDN 厂商、观众地理位置 |
实际体感:连麦双方走纯 RTC 通道,延迟在 200ms 内;观众看混流 + CDN 链路,延迟 1-3s。
几个优化方向:
- 混流输出总码率不要过高(800-1500 kbps 足够),码率越高在弱网下 CDN 分发越容易卡顿
- 如果观众端也走 RTC 直拉(小规模场景),连麦者和主播将
<live-player>的mode设为RTC,延迟可控制在 400ms 内 minPlayStreamBufferLength参数可以在卡顿率和延迟之间做取舍(值越大越流畅但延迟越高)
总结
一条完整的直播连麦链路,最基础的调用只有三个 API:
| API | 干什么 |
|---|---|
loginRoom | 进入房间 |
startPublishingStream | 推自己的流 |
startPlayingStream | 拉别人的流 |
连麦本身只需这三步:1 个主播 + 1 个连麦者,每人互拉对方的流即可工作。混流和 CDN 转推是规模扩大后的追加选项:
| API | 什么时候加 |
|---|---|
startMixerTask | 连麦者 ≥ 2 或观众端需要降负载时,合路减少拉流数量 |
addPublishCdnUrl | 观众数量 > 100 级,需要 CDN 做大规模分发时 |
ZEGO Express SDK 扛掉了信令通道、SFU 转发、NAT 穿透、弱网对抗、混流服务。开发者只需要关注流的管理和组件渲染。
延伸阅读:
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/yinshipin/70457.html