电竞直播的系统架构、核心功能实现与关键优化策略

一、引言

电竞直播是实时性与观赏性要求最苛刻的直播场景之一。一场《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 → 百万级并发 → 全球边缘节点加速        │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

客户端层包含三类角色:

  1. OB 推流端(通常是一台高配 PC):运行游戏,通过屏幕共享采集画面,同时作为主推流端向 RTC 房间推流。在推流的同时发送 SEI 数据(比分、时间轴)。如果游戏本身在高配 PC 上运行,OB 推流端可以使用硬件编码减少性能消耗。
  2. 解说员端(1-4 人的 Web 或桌面客户端):通过摄像头采集视频画面,麦克风采集音频,加入同一个 RTC 房间进行实时交流。解说员的音频流推送至连麦房间,视频画面作为混流的输入源之一。
  3. 观众端(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 使用注意事项

  1. SEI 不保证可靠传输。如果某个视频帧丢失,附带的 SEI 数据也一同丢失。业务层需要处理 SEI 缺失的场景,通过 seq 和心跳包机制检测丢包。
  2. CDN 场景的 SEI:通过 CDN 播放时,第三方播放器默认不支持 SEI 解析。需要选用支持自定义 SEI 解析的播放器(如 flv.js 配合自定义脚本),或者改用独立的数据通道(如 ZIM)传递比分数据。
  3. 数据量控制: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 进行裁剪和转码,生成短视频片段

八、总结

  1. 超低延迟是电竞直播的生命线。RTC 技术将端到端延迟从传统直播的 3-10 秒压缩到 200-1000ms,让观众真正”与赛场同步”。屏幕共享 + 60fps 高帧率编码保证了游戏画面的流畅度,是 FPS/MOBA 类电竞直播的硬性门槛。
  2. 架构设计的核心是分层解耦。游戏画面采集、解说连麦、云端混流、CDN 分发、弹幕互动各自使用独立的通道和能力,通过混流任务将多路输入合成为一路输出。不要试图在一个模块里解决所有问题。
  3. SEI 解决了”数据与画面同步”的终极问题,但需要业务层做好容错。SEI 不保证可靠传输,必须配合序列号、心跳包和独立数据通道做兜底。比分数据的关键变更应通过可靠通道(ZIMCustomMessage)同步发送。
  4. 消息通道选择遵循”高并发用不可靠通道,关键业务用可靠通道”原则。弹幕走 ZIMBarrageMessage(不限频、不可靠),信令走 sendCustomCommand(可靠),事件广播走 sendBroadcastMessage(可靠),帧同步数据走 sendSEI(不可靠但有心跳兜底)。

技术选型一句话总结:以 ZEGO Express SDK 构建 RTC 推拉流、屏幕共享、SEI 和混流能力,以 ZIM SDK 承载弹幕和业务消息通道,通过 CDN 旁路推流实现百万级并发分发,是当前电竞直播场景最为工程化可行的技术方案。

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/info/67375.html

(0)

相关推荐