一、引言
电竞直播是实时性与观赏性要求最苛刻的直播场景之一。一场《CS2》Major 决赛或《英雄联盟》全球总决赛,同时在线观众可达数百万。观众对直播体验的容忍度极低:画面卡顿哪怕只有两秒,弹幕就会刷满”卡了”;比赛结果如果比隔壁直播间晚 3 秒,观众就会流失。
从技术角度看,电竞直播不是”把游戏画面推出去”那么简单。它涉及多个核心难题:游戏画面以 60fps 高帧率采集和编码,对编码性能和带宽都是巨大考验;异地解说员通过远程连麦协作解说,不同解说员之间的网络延迟差异会导致”两名解说抢话”的效果;比赛关键时刻的带宽峰值可能让 CDN 边缘节点过载;此外还需要叠加实时比分、选手数据等增强信息,要求与画面帧级同步。
实时音视频(RTC)技术的介入,从根本上改变了传统电竞直播的技术栈。相比传统 RTMP/HLS 方案 3-10 秒的延迟,RTC 方案可将端到端延迟压缩到 200-1000ms,实现”毫秒级同频”;屏幕共享 API 可以无损采集 60fps 游戏画面;SEI(媒体补充增强信息)机制能在视频帧中嵌入实时比分数据,做到画面与数据的帧级同步;云端混流将游戏画面、解说画面、比分信息合成一路流后通过 CDN 分发,观众端只需拉一路流即可完整体验。
本文面向直播和 RTC 开发者,结合即构科技的实时音视频 SDK(ZEGO Express SDK)的实际能力,介绍电竞直播场景下的系统架构、核心功能实现与关键优化策略。
二、场景技术需求拆解
电竞直播场景的技术需求可以按”观众侧体验”和”制播侧能力”两个维度拆解。每一项需求直接对应一项具体的工程指标。
2.1 核心需求矩阵
| 需求项 | 具体描述 | 技术指标 | 优先级 |
|---|---|---|---|
| 游戏画面采集 | 采集选手或 OB 视角的游戏画面,要求高帧率、高分辨率,FPS 游戏不能低于 60fps | 1080p/60fps 以上,采集帧率稳定 | 必需 |
| 超低延迟传输 | 比赛画面从推流端到观众端的延迟必须足够低,避免观众被剧透 | 端到端延迟 < 1s(RTC 模式),CDN 分发 < 3s | 必需 |
| 多路解说连麦 | 支持 2-4 名异地解说员实时音频交流,解说员之间互相听到对方 | 解说员间音频延迟 < 200ms | 必需 |
| 云端混流 | 将游戏画面、解说摄像头画面、比分牌等多路画面合成一路流 | 支持自定义布局、多输入流、多输出流 | 必需 |
| CDN 大规模分发 | 混流后推送到 CDN,支持百万级并发观看 | 支持 RTMP/FLV/HLS 多协议分发 | 必需 |
| SEI 实时数据叠加 | 在视频流中嵌入比赛时间轴、比分、选手 KDA 等数据,要求与画面帧级同步 | SEI 发送频率与视频帧率对齐 | 必需 |
| 弹幕互动 | 观众发送弹幕,实时显示在直播间 | 支持高并发、不限频发送 | 必需 |
| 3A 音频处理 | 解说员侧的噪声抑制、回声消除、自动增益控制 | AI 降噪、AEC 双讲通透 | 必需 |
| 防作弊与画面安全 | 防止选手通过直播画面获取对手信息(如小地图暴露) | 画面延迟可控、水印溯源、区域遮挡 | 加分 |
| 多视角切换 | 观众可自主切换主 OB 视角、选手第一视角、数据面板 | 多流同步拉取、流切换延迟 < 500ms | 加分 |
| 云端录制与回放 | 比赛全程录制,支持回放和高光剪辑 | 单流/混流录制,录制文件即时可用 | 加分 |
| 质量监控 | 全链路推拉流质量数据,用于实时告警和事后分析 | 分辨率、帧率、码率、卡顿率、丢包率等指标 | 加分 |
2.2 需求优先级说明
- 必需:无此能力则产品不可用。例如,如果端到端延迟超过 5 秒,弹幕环境下的观赛体验会完全崩塌。
- 加分:可显著提升产品竞争力,但缺乏时不影响基础观赛体验。多视角切换是典型的加分项 —— 大部分赛事直播只有 OB 视角,但提供多视角的产品在体验上形成明显差异化。
三、RTC 技术选型
3.1 自建 vs 成熟 RTC SDK
| 维度 | 自建方案(WebRTC + SFU + CDN) | 成熟 RTC SDK |
|---|---|---|
| 开发周期 | 6-12 个月 | 2-4 周集成 |
| 延迟 | 可实现 200-500ms(需深度优化) | 200ms(RTC 推拉流)/ 600-1000ms(超低延迟直播) |
| 屏幕共享高帧率 | 需自行处理 getDisplayMedia 编码参数调优 | SDK 封装 + 预设档位 + 自定义参数 |
| 混流能力 | 需自建 MCU 混流服务或客户端合成 | 云端混流 + 本地导播插件,服务端 API 调用即可 |
| CDN 旁路 | 需自建转推服务,适配多家 CDN | SDK 一行 API 转推,统一接入多家 CDN |
| SEI 传输 | 需自行实现 SEI 编码/解析 | SDK 原生支持 sendSEI + playerRecvSEI 回调 |
| 全球网络覆盖 | 需自行部署 SFU 节点 | MSDN 全球节点,就近接入 |
| 运维成本 | 高(需专人维护 SFU/MCU/CDN 链路) | 低(ZEGO 星图全链路监控) |
| 万人连麦 | 需自研级联方案 | ZEGO 原生支持万人连麦 |
对于大多数电竞直播产品,自建方案的投入产出比极低。RTC 是工程密集型领域,全局网络调度、弱网对抗、编解码优化、跨平台兼容等每一项都需要数年积累。选择成熟的 RTC SDK 意味着将底层复杂度外包,研发团队聚焦业务逻辑。
3.2 关键技术指标(以电竞直播为基准)
| 指标 | 目标值 | 说明 |
|---|---|---|
| 推流端到 RTC 服务器延迟 | < 100ms | 取决于推流端网络上行 |
| RTC 拉流端延迟 | 200ms | Express SDK 默认水平 |
| 超低延迟直播延迟 | 600-1000ms | 大规模分发场景 |
| CDN 分发延迟 | 1-3s | FLV/RTMP 协议 |
| 屏幕共享帧率 | 60fps(可配置) | 自定义 quality=4 模式 |
| 屏幕共享分辨率 | 1080p(可配置) | 取决于推流端设备性能 |
| 音频延迟(解说连麦) | < 200ms | RTC 房间内音频 |
| 弹幕并发 | 不限频 | ZIMBarrageMessage / Express sendBarrageMessage |
| 混流并发任务 | 按 AppID 限制 | 服务端混流 API 调用频率限制 |
| 单房间观众并发 | 百万级(CDN 分发) | 经过 CDN 旁路后无房间人数限制 |
3.3 ZEGO Express SDK 能力匹配
对照上述需求,Express SDK 提供的能力矩阵:
- 屏幕共享:支持自定义 quality 档位(1=流畅/2=平衡/3=清晰/4=自定义),在 quality=4 模式下可指定 frameRate、bitrate、width、height,覆盖 1080p/60fps 的高帧率游戏采集需求。
- 超低延迟直播:基于 ZEGO 自研私有协议,端到端延迟 600-1000ms,支持 4K/60fps/HDR,在大码率游戏场景中根据观众带宽自适应转码。
- SEI 消息:sendSEI 接口在推流成功后调用,将自定义字节数据(比分、时间轴等)嵌入视频流。拉流端通过 playerRecvSEI 回调接收,与视频帧同步解析。
- 云端混流:startMixerTask 接口发起手动混流任务,自定义 inputList(输入流布局)、outputList(输出流 ID)、outputConfig(输出编码参数),完美覆盖”游戏画面+解说画面+比分牌”的合成需求。
- CDN 旁路推流:addPublishCdnUrl 接口单行调用,将 RTC 房间内的流转推到 CDN,支持 RTMP/FLV/HLS 多协议分发。
- 房间消息:sendBarrageMessage(弹幕,不限频,不保证可靠)/ sendBroadcastMessage(广播,10条/秒,可靠)/ sendCustomCommand(信令,30条/秒,可靠)三条消息通道满足不同业务需求。
四、系统架构设计
4.1 整体架构
电竞直播系统分为四层:客户端层、RTC 网络层、业务服务层、CDN 分发层。
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ OB 推流端 │ │解说员 A 端│ │解说员 B 端│ │ 观众 Web 端 │ │
│ │ (屏幕共享) │ │ (摄像头+麦)│ │ (摄像头+麦)│ │ (拉流+弹幕) │ │
│ └─────┬────┘ └─────┬────┘ └─────┬────┘ └──────┬─────┘ │
│ │ │ │ │ │
└────────┼─────────────┼─────────────┼──────────────┼─────────┘
│ │ │ │
▼ ▼ ▼ │
┌─────────────────────────────────────────────────────────────┐
│ RTC 网络层 (ZEGO Express) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 屏幕共享流 │ │ 解说音频流 │ │ SEI 数据 │ │ 弹幕/信令 │ │
│ │ (大流60fps)│ │ (连麦房间) │ │ (帧级同步)│ │ (消息通道) │ │
│ └─────┬────┘ └─────┬────┘ └─────┬────┘ └──────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┼─────────────┘ │ │
│ ▼ │ │
│ ┌──────────────┐ │ │
│ │ 云端混流服务 │ │ │
│ │ (多入单出/多出)│ │ │
│ └──────┬───────┘ │ │
│ │ │ │
└─────────────────────┼─────────────────────────────┼─────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 业务服务层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 赛事管理 │ │ 比分系统 │ │ Token 鉴权│ │ 录制管理 │ │
│ │ (赛程/战队)│ │ (实时数据)│ │ (AppSign)│ │ (云端录制) │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CDN 分发层 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ RTMP/FLV/HLS → 百万级并发 → 全球边缘节点加速 │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
客户端层包含三类角色:
- OB 推流端(通常是一台高配 PC):运行游戏,通过屏幕共享采集画面,同时作为主推流端向 RTC 房间推流。在推流的同时发送 SEI 数据(比分、时间轴)。如果游戏本身在高配 PC 上运行,OB 推流端可以使用硬件编码减少性能消耗。
- 解说员端(1-4 人的 Web 或桌面客户端):通过摄像头采集视频画面,麦克风采集音频,加入同一个 RTC 房间进行实时交流。解说员的音频流推送至连麦房间,视频画面作为混流的输入源之一。
- 观众端(Web/小程序/App):从 CDN 拉取混流后的直播画面,通过 ZIM SDK 或 Express 弹幕通道发送/接收弹幕,解析 SEI 回调中的实时数据渲染比分面板。
4.2 数据流设计
| 数据类型 | 传输方式 | 可靠性要求 | 频率/带宽 | 延迟要求 |
|---|---|---|---|---|
| 游戏画面(大流) | Express 推流 → 云端混流 → CDN | 可靠 | 1080p/60fps, ~6000-15000kbps | < 3s |
| 解说员音频 | Express 音频推流 → 自动混流 | 可靠 | 48kbps/路 | < 200ms(解说间) |
| 解说员视频 | Express 视频推流 → 混流输入 | 可靠 | 720p/30fps, ~1500kbps | 与游戏画面自然对齐 |
| 实时比分/时间轴 | Express sendSEI | 不可靠(随帧丢失),业务层容错 | 每帧 ≤ 4KB payload | 帧级同步 |
| 观众弹幕 | ZIMBarrageMessage / Express sendBarrageMessage | 不可靠,允许丢失 | 不限频,单条 ≤ 5KB | < 500ms |
| 赛事元数据(赛程/阵容) | ZIMCustomMessage / HTTP API | 可靠有序 | 低频(赛事开始/结束触发) | 无强要求 |
| 解说员信令(开麦/闭麦) | Express sendCustomCommand | 可靠 | 30 条/秒 | < 200ms |
4.3 消息通道选型策略
电竞直播场景涉及多条消息通道,合理选择通道类型直接影响系统的可靠性和成本:
| 场景 | 推荐通道 | 理由 |
|---|---|---|
| 观众弹幕 | ZIMBarrageMessage(ZIM SDK,不限频、不可靠) | 弹幕量大(峰值可到数十万条/秒),允许少量丢失,不需要可靠投递;ZIM SDK 的弹幕消息专门优化了高并发场景 |
| 解说员控制信令(麦位切换/闭麦提示) | sendCustomCommand(Express 自带,30条/秒,可靠) | 信令量小、要求可靠送达,Express 自带消息无需额外 SDK |
| 赛事状态广播(比赛开始/暂停/结束) | sendBroadcastMessage(Express 自带,10条/秒,1024字节,可靠) | 关键状态变更需要保证全房间送达 |
| 实时比分数据 | sendSEI(Express,帧级同步) | 比分需要与画面严格对齐,SEI 是唯一能做到帧级同步的通道 |
| 历史赛程/战队数据查询 | HTTP REST API + 业务数据库 | 静态数据读取,不需要实时通道 |
| 弹幕高级功能(撤回/礼物/禁言) | ZIMTextMessage + ZIMCustomMessage(可靠有序) | 需要可靠送达和有序处理的业务逻辑 |
策略核心原则:高并发、可丢失的消息用 ZIM 弹幕通道 / Express 弹幕通道;必须可靠送达的信令用 Express 广播或自定义信令;与视频帧绑定的数据必须走 SEI;需要历史存储的业务消息走 ZIM 可靠消息通道。
五、核心功能实现
以下代码示例基于 ZEGO Express Web SDK 和 ZIM Web SDK,展示电竞直播场景中四个核心功能的实现。
5.1 游戏画面屏幕共享(60fps 高帧率采集)
高帧率屏幕共享是电竞直播的技术基石。FPS 游戏(如 CS2、Valorant)通常以 144fps 甚至 240fps 运行,如果采集帧率只有 15fps,观众看到的画面会严重卡顿和不连贯。
Express SDK 的屏幕共享在 quality=4 模式下支持自定义帧率、码率和分辨率。以下是面向 1080p/60fps 游戏直播的采集配置:
// ========================================================
// 电竞直播 - 高帧率游戏画面屏幕共享采集
// 目标:1080p @ 60fps,适合 FPS/MOBA 游戏直播
// ========================================================
const zg = new ZegoExpressEngine(appID, server);
// 1. 登录房间(OB推流端作为主播加入)
await zg.loginRoom(
'esports_room_001',
token,
{ userID: 'ob_caster_01', userName: 'OB_Caster' }
);
// 2. 创建屏幕共享流 - 自定义参数模式(quality=4)
// ZegoCaptureScreenVideo 自定义参数说明:
// - quality: 4 表示自定义模式
// - width/height: 输出分辨率,推荐 1920x1080
// - frameRate: 采集帧率,FPS 游戏推荐 60,MOBA 可用 30
// - bitrate: 视频码率(kbps),1080p/60fps 推荐 6000-15000
// 高动态画面(FPS)取上限,相对静态(MOBA)取下限
const gameStream = await zg.createZegoStream({
videoBitrate: 12000, // 视频码率 12Mbps
audioBitrate: 128, // 游戏音频码率 128kbps
screen: {
audio: true, // 采集系统声音(游戏音效)
video: {
quality: 4, // 自定义模式
width: 1920, // 输出分辨率宽
height: 1080, // 输出分辨率高
frameRate: 60, // 60fps 采集
bitrate: 12000, // 码率 12Mbps
}
}
});
// 3. 开双流模式(大小流),方便弱网观众自动切换小流
// 大流:1080p/60fps,小流:540p/15fps
zg.enableDualStream(gameStream);
zg.setLowStreamParameter(gameStream, {
width: 540,
height: 960,
frameRate: 15,
bitRate: 800
});
// 4. 监听推流质量,用于实时监控
zg.on('publishQualityUpdate', (streamID, stats) => {
console.log('[OB端推流质量]', {
streamID,
分辨率: `${stats.video.width}x${stats.video.height}`,
采集帧率: stats.video.captureFPS,
发送帧率: stats.video.sendFPS,
发送码率: `${stats.video.sendBitrate} kbps`,
是否硬编: stats.video.isHardwareEncode,
丢包率: `${stats.video.pktLostRate}%`,
RTT: `${stats.rtt}ms`
});
});
// 5. 开始推流
const publishResult = await zg.startPublishingStream(
'game_main_stream_001',
gameStream,
{
videoCodec: 'H264', // H.264 编码(兼容 CDN 转推)
isSEIStart: true, // 开启 SEI 发送能力
SEIType: 0 // 使用 ZEGO 自定义 SEI 类型
}
);
// 6. 推流成功后,开启 CDN 旁路转推
// 观众将通过 CDN 地址观看混流后的直播
await zg.addPublishCdnUrl(
'game_main_stream_001',
'rtmp://your-push-domain.com/live/game_main_stream_001'
);
关键参数决策说明:
- 码率选择:1080p/60fps 的高动态游戏画面,推荐 8-15Mbps。如果编码端硬件编码能力较弱,可降至 6-8Mbps 并开启低码高清增强。
- 关键帧间隔:屏幕共享默认关键帧间隔为 2 秒。对于游戏场景,建议保持默认值。减小关键帧间隔会增加编码压力和带宽消耗,增大则影响首帧速度和拖动体验。
- 硬件编码:Chrome 下默认开启硬件编码(调用 enableHardwareEncoder),可通过 publishQualityUpdate 回调中的 isHardwareEncode 字段确认。如果硬件编码未生效(部分显卡/驱动组合不支持),系统将回退到软编,此时 1080p/60fps 对 CPU 压力较大,需降配。
5.2 多路解说连麦与音频混流
解说连麦的难点在于:解说员分布在不同城市(甚至不同国家),彼此之间的网络延迟差异会导致”抢话”——解说员 A 以为 B 说完了开始接话,实际上 B 的话还在传输途中。
解决方案是使用同一个 RTC 房间进行音频通话,利用 SDK 内部的 QoS 机制自动管理音频延迟。解说员的音频通过 Express SDK 的自动混流能力合为一路后,作为混流任务的音频输入。
// ========================================================
// 解说员端 - 加入连麦房间 + 配置音频参数
// ========================================================
// 解说员 A 加入连麦房间
const commentatorA = new ZegoExpressEngine(appID, server);
// 配置 3A 音频参数(噪声抑制、回声消除、自动增益)
// 解说员场景的音频要求:
// - AI降噪开启:过滤键盘声、空调声等环境噪音
// - AEC开启:但需要在"双讲"场景(两人同时说话)时保持通透
// - ANS开启:抑制环境噪声
// - AGC关闭:避免压缩解说员的声音动态
commentatorA.setAudioConfig({
ANS: true, // 自动噪声抑制
AEC: true, // 回声消除(解说员通常戴耳机,但保留以防万一)
AGC: false, // 解说场景不建议开启 AGC,会压缩声音动态
AIANS: true // AI 降噪(比传统 ANS 更好地过滤非人声)
});
// 创建摄像头流(解说员画面)
const commentatorStream = await commentatorA.createZegoStream({
camera: {
audio: {
channelCount: 1, // 单声道(解说不需要立体声)
sampleRate: 48000, // 48kHz 采样率
bitrate: 64 // 音频码率 64kbps
},
video: {
quality: 2, // 720p/20fps 平衡模式
}
}
});
// 加入同一个 RTC 房间
await commentatorA.loginRoom(
'esports_room_001',
commentatorToken,
{ userID: 'commentator_A', userName: '解说员A' }
);
// 推流(音频+视频)
await commentatorA.startPublishingStream(
'commentator_A_stream',
commentatorStream
);
// 监听其他解说员的流加入
commentatorA.on('roomStreamUpdate', async (roomID, updateType, streamList) => {
if (updateType === 'ADD') {
for (const stream of streamList) {
// 拉取其他解说员的音频流
if (stream.streamID.startsWith('commentator_')) {
const remoteStream = await commentatorA.startPlayingStream(
stream.streamID
);
// 播放到本地音频输出(解说员监听)
// remoteAudioElement.srcObject = remoteStream;
}
}
}
});
多路解说混流方案:解说员的音频推荐使用 ZEGO 的自动混流功能,无需手动管理音频流列表。自动混流将房间内所有音频流合并为一路,开发者只需处理视频画面的混流布局。
// ========================================================
// 混流控制端(服务端或导播客户端)
// ========================================================
// 启动混流任务 - 电竞直播布局示例
// 布局:游戏画面全屏 + 解说A画面(右下小窗) + 解说B画面(右中小窗) + 比分牌(覆盖)
const mixResult = await zg.startMixerTask({
taskID: 'esports_mix_001',
inputList: [
// 第一层:游戏画面(全屏背景)
{
streamID: 'game_main_stream_001',
contentType: 'VIDEO',
layout: {
top: 0,
left: 0,
bottom: 1080,
right: 1920
}
},
// 第二层:解说员A画面(右下角小窗)
{
streamID: 'commentator_A_stream',
contentType: 'VIDEO',
layout: {
top: 780,
left: 1560,
bottom: 1060,
right: 1900
}
},
// 第三层:解说员B画面(右下角第二个小窗)
{
streamID: 'commentator_B_stream',
contentType: 'VIDEO',
layout: {
top: 600,
left: 1560,
bottom: 760,
right: 1900
}
},
// 音频:自动混流房间内所有解说员音频
// 通过自动混流方式,无需在 inputList 中单独指定音频流
],
// 输出配置
outputList: ['esports_mixed_output_stream'],
outputConfig: {
outputWidth: 1920,
outputHeight: 1080,
outputFPS: 60,
outputBitrate: 15000
}
});
// 混流输出流推送到 CDN
await zg.addPublishCdnUrl(
'esports_mixed_output_stream',
'rtmp://your-push-domain.com/live/esports_final'
);
5.3 SEI 发送实时比分与时间轴数据
SEI(Supplemental Enhancement Information,媒体补充增强信息)是在 H.264/H.265 视频码流中嵌入自定义数据的标准机制。在电竞直播中,SEI 的核心价值在于帧级同步——你的比分数据与视频画面的某个关键帧严格绑定,观众在任何时刻看到的比分一定是与当前画面一致的。
典型的 SEI 使用场景包括:
– 实时比分(如 CS2 的 CT vs T 比分、回合数)
– 比赛时间轴(游戏内倒计时、加时赛时间)
– 选手 KDA 数据(随 OB 切换选手瞬间更新)
– 地图/位置信息(当前画面展示的是地图的哪个区域)
// ========================================================
// 推流端(服务端比分系统 → 推流客户端)发送 SEI
// ========================================================
// 模拟:比分系统回调,当比赛数据变化时触发
function onScoreUpdate(gameData) {
// 构造 SEI 数据包(二进制格式,紧凑编码)
// 数据格式:[type(1B)][seq(2B)][timestamp(4B)][payload_len(2B)][payload(var)]
const seiPayload = buildScoreSEIPacket({
matchId: gameData.matchId,
teamA: {
name: gameData.teamA.name,
score: gameData.teamA.score // 比分
},
teamB: {
name: gameData.teamB.name,
score: gameData.teamB.score
},
roundNumber: gameData.roundNumber, // 当前回合
roundTime: gameData.roundTime, // 回合剩余时间(秒)
bombStatus: gameData.bombStatus, // 炸弹状态(CS2): 0=未放置/1=已放置/2=已拆除
mapName: gameData.mapName, // 地图名称
players: gameData.players.map(p => ({
id: p.id,
name: p.name,
kills: p.kills,
deaths: p.deaths,
assists: p.assists,
isAlive: p.isAlive,
weapon: p.weapon,
hp: p.hp
}))
});
// 发送 SEI(需要在推流成功后调用)
// sendSEI 接口限制:单次 payload 大小限制取决于 SDK 版本,
// 建议控制在 4KB 以内
zg.sendSEI('game_main_stream_001', seiPayload);
}
// 构建二进制 SEI 数据包
function buildScoreSEIPacket(data) {
// 使用 JSON → Uint8Array(简化示例,生产建议用 Protobuf 或自定义二进制格式)
const jsonStr = JSON.stringify(data);
const encoder = new TextEncoder();
const bytes = encoder.encode(jsonStr);
// 在实际使用中,建议用更紧凑的二进制编码以减少 SEI 开销
// 例如:将 playerID 映射为 uint8,将 kills/deaths 编码为 uint8...
return bytes;
}
// 高频发送策略:比分变化时立即发送,无变化时每 2 秒发送一次心跳
// 这是为了处理 SEI 丢帧(SEI 随帧传输,不保证可靠),观众端可以通过
// 心跳包判断 SEI 数据是否中断
let seiSeq = 0;
function sendScoreSEIWithHeartbeat(gameData) {
seiSeq++;
// 构建数据包,包含 seq 用于观众端检测丢包
const packet = buildScoreSEIPacket({
...gameData,
_seq: seiSeq,
_timestamp: Date.now()
});
zg.sendSEI('game_main_stream_001', packet);
}
// 心跳定时器:每 2 秒发送一次(即使数据未变化)
const heartbeatTimer = setInterval(() => {
if (currentGameData) {
sendScoreSEIWithHeartbeat(currentGameData);
}
}, 2000);
// ========================================================
// 观众端 - 接收 SEI 数据并渲染实时比分面板
// ========================================================
const viewer = new ZegoExpressEngine(appID, server);
// 拉流前注册 SEI 回调
viewer.on('playerRecvSEI', (streamID, uintArray) => {
// uintArray 为 Uint8Array 类型的 SEI 载荷数据
// 跳过前 4 字节的 mediaSideInfoType
// 1004 = payload type 5 (UserUnregister SEI)
// 1005 = payload type 243 (ZEGO 自定义 SEI)
let offset = 4;
// 解析业务 SEI 数据(与推流端编码格式对应)
const seiData = new Uint8Array(uintArray.buffer, offset);
const jsonStr = new TextDecoder().decode(seiData);
try {
const gameData = JSON.parse(jsonStr);
updateScorePanel(gameData);
} catch (e) {
console.error('SEI 解析失败:', e);
}
});
// 更新比分面板 UI
function updateScorePanel(data) {
// 队伍比分
document.getElementById('team-a-score').textContent = data.teamA.score;
document.getElementById('team-b-score').textContent = data.teamB.score;
// 回合信息
document.getElementById('round-info').textContent =
`Round ${data.roundNumber} | Time: ${data.roundTime}s`;
// 选手实时数据表
updatePlayerStatsTable(data.players);
// 炸弹状态指示(CS2 场景)
if (data.bombStatus !== undefined) {
const bombIndicator = document.getElementById('bomb-status');
if (data.bombStatus === 1) {
bombIndicator.textContent = 'C4 PLANTED';
bombIndicator.className = 'bomb-alert';
} else {
bombIndicator.textContent = '';
}
}
}
// 开始拉流(需要开启 SEI 解析)
await viewer.loginRoom('esports_room_001', viewerToken,
{ userID: 'viewer_001', userName: '观众001' }
);
// 观众拉取混流后的 CDN 流
// 注意:通过 CDN 播放时,SEI 需要使用第三方播放器自行解析
// RTC 直拉流可以直接通过 playerRecvSEI 回调接收
await viewer.startPlayingStream('esports_mixed_output_stream', {
isSEIStart: true // 必须开启,否则 SDK 不会解析 SEI
});
SEI 使用注意事项:
- SEI 不保证可靠传输。如果某个视频帧丢失,附带的 SEI 数据也一同丢失。业务层需要处理 SEI 缺失的场景,通过 seq 和心跳包机制检测丢包。
- CDN 场景的 SEI:通过 CDN 播放时,第三方播放器默认不支持 SEI 解析。需要选用支持自定义 SEI 解析的播放器(如 flv.js 配合自定义脚本),或者改用独立的数据通道(如 ZIM)传递比分数据。
- 数据量控制:SEI 随每帧发送,payload 过大会挤占视频带宽。建议单次 SEI payload 控制在 1KB 以内,高频更新数据(如玩家坐标)考虑只发送变化量而非全量数据。
5.4 观众弹幕互动
弹幕是电竞直播中观众参与感的核心载体。大型赛事的下半场关键回合,弹幕量可达每秒数万条。选择一个能承载高并发的弹幕通道至关重要。
ZIM SDK 的 ZIMBarrageMessage 是专门为弹幕场景设计的消息类型,不限频、不保证可靠、不存储历史。单条消息不超过 5KB,与电竞直播的弹幕需求完全匹配。
// ========================================================
// 观众端 - 弹幕发送与接收(使用 ZIM SDK)
// ========================================================
import ZIM from 'zego-zim-web';
const zim = ZIM.create({ appID, server });
// 观众加入 ZIM 房间(与 RTC 房间独立管理)
// ZIM 房间用于弹幕和聊天,RTC 房间用于音视频流
await zim.login({
userID: 'viewer_001',
userName: '电竞观众A',
token: zimToken
}, 'esports_chat_room_001');
// ---- 发送弹幕 ----
function sendBarrage(text, color = 'white', position = 'scroll') {
const barrageData = JSON.stringify({
type: 'barrage',
text: text,
color: color, // 弹幕颜色
position: position, // 'scroll' 滚动 / 'top' 顶部固定 / 'bottom' 底部
size: 'normal', // 字体大小
timestamp: Date.now()
});
// ZIMBarrageMessage: 不可靠、不限频
const barrageMsg = new ZIMBarrageMessage({
message: barrageData
});
zim.sendMessage(barrageMsg, {
roomID: 'esports_chat_room_001',
conversationType: ZIM.enums.ConversationType.Room
});
}
// ---- 接收弹幕 ----
zim.on('receiveRoomMessage', (messageList, roomID) => {
for (const msg of messageList) {
if (msg.type === ZIM.enums.MessageType.Barrage) {
const barrageData = JSON.parse(msg.message);
// 渲染弹幕到屏幕上(弹幕引擎)
renderBarrageOnCanvas({
text: barrageData.text,
color: barrageData.color,
position: barrageData.position,
from: msg.fromUserName // 发送者昵称
});
}
}
});
// ---- 弹幕渲染(Canvas 实现) ----
const barrageCanvas = document.getElementById('barrage-layer');
const ctx = barrageCanvas.getContext('2d');
const barrageQueue = [];
function renderBarrageOnCanvas(barrage) {
barrageQueue.push({
...barrage,
x: barrageCanvas.width, // 从右侧开始滚动
y: Math.random() * (barrageCanvas.height - 30),
speed: 2 + Math.random() * 2
});
}
function animateBarrage() {
ctx.clearRect(0, 0, barrageCanvas.width, barrageCanvas.height);
for (let i = barrageQueue.length - 1; i >= 0; i--) {
const b = barrageQueue[i];
b.x -= b.speed;
if (b.x < -ctx.measureText(b.text).width) {
barrageQueue.splice(i, 1); // 移出屏幕,移除
continue;
}
ctx.fillStyle = b.color;
ctx.font = '24px sans-serif';
ctx.fillText(b.text, b.x, b.y);
}
requestAnimationFrame(animateBarrage);
}
animateBarrage();
Express SDK 自带弹幕通道 vs ZIM SDK 弹幕通道的选型:
| 维度 | Express sendBarrageMessage | ZIM ZIMBarrageMessage |
|---|---|---|
| 限频 | 20 条/秒(单个用户) | 不限频 |
| 大小限制 | 默认 1KB | 5KB |
| 是否需要额外 SDK | 否(Express 自带) | 是(需额外集成 ZIM SDK) |
| 房间管理 | 与 RTC 房间绑定 | 独立房间管理(更灵活) |
对于弹幕量较大的赛事,推荐使用 ZIMBarrageMessage。Express 自带的 sendBarrageMessage 有 20 条/秒的限频,单观众在关键回合可能不够用(连续刷屏),且不支持业务层需要的弹幕高级功能(如撤回、敏感词过滤回调等)。
5.5 动态码率调整与弱网对抗
电竞直播的观众分布在全球各地,网络环境千差万别。为确保所有观众都能流畅观赛,推流端需要根据实际网络状况动态调整码率。
// ========================================================
// 推流端 - 动态码率调整策略
// ========================================================
let currentBitrate = 12000; // 初始 12Mbps
const BITRATE_STEPS = [4000, 6000, 8000, 10000, 12000, 15000];
// 监听推流质量,根据丢包率和 RTT 自动调整码率
zg.on('publishQualityUpdate', (streamID, stats) => {
const { video, rtt } = stats;
// 弱网检测逻辑
const pktLostRate = video.pktLostRate || 0;
const currentFPS = video.sendFPS || 0;
if (pktLostRate > 5 || rtt > 300) {
// 网络恶化:降码率
const currentIdx = BITRATE_STEPS.indexOf(currentBitrate);
if (currentIdx > 0) {
// 降一档
const newBitrate = BITRATE_STEPS[currentIdx - 1];
console.warn(`[弱网] 丢包率 ${pktLostRate}%, RTT ${rtt}ms, 降码率至 ${newBitrate}kbps`);
zg.setVideoConfig(gameStream, {
maxBitrate: newBitrate,
frameRate: Math.max(30, currentFPS - 5) // 同时降帧率
});
currentBitrate = newBitrate;
}
} else if (pktLostRate < 1 && rtt < 100 && currentFPS >= 55) {
// 网络良好:恢复码率
const currentIdx = BITRATE_STEPS.indexOf(currentBitrate);
if (currentIdx < BITRATE_STEPS.length - 1) {
const newBitrate = BITRATE_STEPS[currentIdx + 1];
console.log(`[网络恢复] 升码率至 ${newBitrate}kbps`);
zg.setVideoConfig(gameStream, {
maxBitrate: newBitrate,
frameRate: 60
});
currentBitrate = newBitrate;
}
}
});
六、关键问题与优化策略
| 问题 | 根因分析 | 优化策略 |
|---|---|---|
| FPS 游戏高帧率屏幕共享编码性能瓶颈 | 60fps 1080p 的 H.264 编码对 CPU/GPU 压力极大。软编时单核 CPU 占用可超过 60%,导致游戏本身帧率下降。部分显卡/驱动组合不支持 Chrome 硬件编码 | 1. 优先确认硬件编码已开启(enableHardwareEncoder),通过 publishQualityUpdate.isHardwareEncode 验证;2. 硬件编码不支持时,将分辨率降至 720p/60fps 或 1080p/30fps;3. 使用 VP8 编码代替 H.264(VP8 编码效率稍低但 CPU 占用更均匀);4. 在独立推流机上采集游戏画面(双机推流方案),推流机与游戏机通过采集卡连接 |
| 异地解说员网络延迟对齐 | 解说员 A 在北京(RTT 30ms),解说员 B 在洛杉矶(RTT 180ms),两人互相听到对方的时间差造成”抢话”和节奏混乱 | 1. 解说员使用同一加速节点:服务端 API 指定所有解说员接入同一区域的 MSDN 节点;2. 在解说台 UI 上显示网络延迟指示器,让解说员感知到对方的延迟;3. 引入”解说队列”机制:通过信令通道传递”我要说话”信号,在 UI 上提示当前说话权持有者;4. 实际上 200ms 以内的延迟差通过解说员的职业素养即可适应 |
| 比赛关键时刻带宽峰值 | 决赛关键时刻(如 CS2 的 1v1 残局),CDN 观众涌入量瞬间激增,边缘节点带宽可能被打满 | 1. 开启大小流(enableDualStream):优质网络观众拉大流,弱网观众自动降级到小流;2. 设置小流参数为 540p/15fps/800kbps,大幅降低弱网观众的带宽压力;3. CDN 使用多厂商覆盖,通过 DNS 智能调度分散流量;4. HLS 切片配合预加载,将峰值带宽需求平滑化 |
| SEI 数据丢失导致比分显示错乱 | SEI 随视频帧传输,丢帧时 SEI 一同丢失。如果比分从 1:0 变为 2:0 但中间的 SEI 丢失,观众可能看到错误的比分 | 1. 在 SEI payload 中携带递增序列号(seq),观众端检测到 seq 跳跃时通过 HTTP 请求服务端全量比分数据做纠错;2. 每 2 秒发送一次包含全量数据的 SEI 心跳包;3. 关键比分变化事件(如得分、比赛结束)通过独立可靠通道(ZIMCustomMessage)同时推送,作为 SEI 的兜底;4. 观众端 UI 层做”乐观更新”:SEI 数据中断时显示”数据同步中”而非过时数据 |
| 防止作弊与画面泄露 | 选手可能通过观看直播画面获取对手位置(”看直播作弊”);教练/分析师团队也关注战术信息泄露 | 1. 直播流增加可控延迟(3-10 秒),选手无法通过直播获取实时信息;2. 开启云端水印(通过混流在画面上叠加半透明 ID),用于泄露溯源;3. 敏感区域遮挡:在混流布局中对小地图、经济面板等敏感信息做半透明遮罩或延迟渲染(需要自定义混流输入预处理);4. 选手摄像头画面拉独立流(不混入公开直播流),仅供内部 OB 系统使用 |
| 多路解说音画同步 | 解说的视频画面和音频分别通过不同流传输,可能出现”解说口型与声音不同步”的问题 | 1. 解说员的摄像头和麦克风使用同一路推流(createZegoStream 的 camera 模式同时采集 video + audio),不在业务层分离音视频推流;2. 混流时指定同一组音视频流为输入,SDK 内部保证 sync;3. 拉流端使用同一 MediaStream 对象播放,浏览器会自动处理音画同步 |
七、场景延伸与扩展玩法
7.1 AI 实时赛事数据分析与可视化
实现思路:在服务端接入游戏数据 API(如 CS2 的 Game State Integration、Valorant 的 Live Client Data),通过 AI 模型实时分析比赛数据,生成实时胜率预测、选手状态分析、战术路线图等可视化内容。这些可视化内容通过 SEI 通道或自定义渲染层叠加到直播画面上。
技术要点:
– 服务端订阅游戏数据 API → 解析并输入 AI 预测模型 → 生成 JSON 格式的可视化数据
– 通过 ZIMCustomMessage(可靠通道)推送到观众端,观众端用 Canvas 渲染叠加层
– 对于需要与画面严格同步的指标(如选手实时 KDA),仍使用 SEI 通道
7.2 多视角切换(选手第一视角)
实现思路:每名选手的客户端同时作为独立推流端,推送到不同的流 ID。观众端在 UI 上切换视角时,实际上是切换拉取的流。OB 主视角作为默认流,选手视角作为可选流。
技术要点:
– 选手端开启屏幕共享推流作为备选视角(与主 OB 流使用不同 streamID)
– 观众端监控 roomStreamUpdate 回调,动态获取可用的视角列表
– 切换视角时调用 startPlayingStream 拉取对应 streamID,同时 stopPlayingStream 停止当前流
– 切换延迟控制在 500ms 以内(通过预加载和流缓存优化)
7.3 VR/AR 沉浸式观赛
实现思路:在 VR 头显中构建虚拟观赛空间,观众可以在 3D 空间中观看比赛。游戏画面投射到虚拟大屏幕上,其他观众以 Avatar 形式出现在虚拟观众席中。通过 3D 音频(空间音效)实现”坐在不同位置听到不同声音”的沉浸感。
技术要点:
– 使用 Web 端 Express SDK + WebXR API 构建 VR 观赛页面
– 范围音频(Range Audio):模拟虚拟空间中的声音位置和衰减
– 多路流同步:在主画面流之外,额外推一路”虚拟观众席”流
– 观众通过 ZIM SDK 在 VR 空间中发送 3D 弹幕(带空间位置的弹幕)
7.4 观众实时投票预测
实现思路:在比赛的每个回合开始前(或关键时刻),发起实时投票——”你认为这回合谁会赢?”。观众通过弹幕面板或独立投票 UI 进行投票,投票结果实时更新并在直播画面上展示。
技术要点:
– 投票发起通过 ZIMCustomMessage(可靠)推送到所有观众客户端
– 观众投票结果通过 ZIMCustomMessage 回传(或 HTTP API + WebSocket)
– 投票聚合在服务端完成,结果通过 SEI 通道叠加到直播画面
– ZIM 房间属性可用于存储当前投票状态(创建房间时设置 roomAttributes)
7.5 赛事回放与高光时刻自动剪辑
实现思路:比赛全程使用 ZEGO 云端录制服务(单流+混流录制),录制文件存储在云端。赛后利用 AI 模型分析录制文件,自动识别高光时刻(击杀、爆头、残局翻盘等),生成剪辑片段。
技术要点:
– 通过服务端 API 启动云端录制任务(startRecord),指定录制模式为混流+单流
– 录制文件完成后,通过回调通知业务服务端
– 利用 SEI 数据中的比赛事件时间戳(在推流时同步记录),快速定位高光时刻在录制文件中的时间位置
– 调用 FFmpeg 进行裁剪和转码,生成短视频片段
八、总结
- 超低延迟是电竞直播的生命线。RTC 技术将端到端延迟从传统直播的 3-10 秒压缩到 200-1000ms,让观众真正”与赛场同步”。屏幕共享 + 60fps 高帧率编码保证了游戏画面的流畅度,是 FPS/MOBA 类电竞直播的硬性门槛。
- 架构设计的核心是分层解耦。游戏画面采集、解说连麦、云端混流、CDN 分发、弹幕互动各自使用独立的通道和能力,通过混流任务将多路输入合成为一路输出。不要试图在一个模块里解决所有问题。
- SEI 解决了”数据与画面同步”的终极问题,但需要业务层做好容错。SEI 不保证可靠传输,必须配合序列号、心跳包和独立数据通道做兜底。比分数据的关键变更应通过可靠通道(ZIMCustomMessage)同步发送。
- 消息通道选择遵循”高并发用不可靠通道,关键业务用可靠通道”原则。弹幕走 ZIMBarrageMessage(不限频、不可靠),信令走 sendCustomCommand(可靠),事件广播走 sendBroadcastMessage(可靠),帧同步数据走 sendSEI(不可靠但有心跳兜底)。
技术选型一句话总结:以 ZEGO Express SDK 构建 RTC 推拉流、屏幕共享、SEI 和混流能力,以 ZIM SDK 承载弹幕和业务消息通道,通过 CDN 旁路推流实现百万级并发分发,是当前电竞直播场景最为工程化可行的技术方案。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/info/67375.html