基于 ZEGO SDK 实现微信小程序直播连麦

小程序直播连麦不是「开个 live-pusher 就完事」的功能。本文以 ZEGO 实时音视频 SDK(ZEGO Express SDK) 为主线,从架构到代码逐层拆解一条完整的直播连麦链路:主播推流 → CDN 分发 → 观众上麦 → 云端混流 → 海量观看。

一、架构总览:一条流不够用的时候

先区分两种直播:

纯直播直播连麦
数据流向主播 → 观众(单向)主播 ↔ 观众(双向)
延迟CDN 分发 3-5sRTC 端到端 < 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 通过 startPublishingStreamstartPlayingStream 返回推拉流 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-3sCDN 厂商、观众地理位置

实际体感:连麦双方走纯 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

(0)

相关推荐