如何构建一套可落地的实时拍卖直播系统(附JS代码示例)

一、引言

一块和田玉籽料在拍卖师的掌心缓缓翻转,强光手电扫过,脂白色的玉质泛出温润的油光。屏幕另一端,六位藏家同时注意到了皮壳上那处细微的洒金——四位立刻举牌,各自加价。

这不是预录视频,这是一场实时直播拍卖。1200公里外那位买家喊出”加50万”的瞬间,拍卖师的耳朵里听到了,他的落槌也必须在所有人同时听到这句加价之后才能落下。出价指令比拍卖师落槌快,这是拍卖行业运行几百年的底层逻辑。搬到线上之后,”比落槌快”变成了”比传输延迟快”——谁能把出价指令毫秒不差地送到所有人面前,谁就保住了这张拍卖桌的公信力。

直播拍卖是直播场景中最特殊的一类。它不像秀场直播可以容忍偶尔卡顿,不像电商直播可以靠评论区滚动弥补实时性不足,也不像视频会议只要”听清就行”。拍卖的场景约束极其苛刻:拍品的纹理、光泽、包浆、瑕疵必须纤毫毕现,买家依赖视觉判断做出竞价决策;出价指令必须严格有序、不可丢失、不可乱序,因为毫秒级的到达差异就决定了拍品归属;所有竞价过程必须完整存证,为后续交割和争议仲裁提供依据。

这些需求恰好落在实时音视频(RTC)技术的核心能力圈内:超低延迟传输解决出价同步问题,媒体补充增强信息(SEI)实现价格信息的帧级精准叠加,可靠有序消息通道保证竞价指令的顺序一致性,云端录制提供全程审计轨迹。

本文面向直播拍卖系统的设计者和开发者,基于即构科技的 ZEGO Express SDK 和 ZIM SDK 的实际能力,从场景需求拆解、技术选型、架构设计到核心功能实现,一步步说明如何构建一套可落地的实时拍卖直播系统。所有代码示例基于JavaScript/Web端。

二、场景技术需求拆解

2.1 核心需求清单

直播拍卖的技术需求可以分为三个层次:基础体验层(缺了就做不了)、竞价业务层(业务逻辑的准确性保障)、信任增强层(合规与公信力)。

需求类别 需求项 详细说明 优先级
基础体验 超低延迟视频传输 拍卖师动作与买家画面同步,端到端延迟需控制在300ms以内 必需
基础体验 高分辨率画质 拍品细节(纹理、光泽、瑕疵、落款)需要4K或至少1080P高清展现 必需
基础体验 弱光画质增强 珠宝、玉石、瓷器等拍品常在柔和聚光灯下展示,暗部细节不能丢失 必需
竞价业务 出价指令可靠有序 多买家并发出价,服务端和所有客户端接收到的出价顺序必须一致 必需
竞价业务 出价不丢不重 弱网下出价消息不能丢失(少算一次出价)也不能重复(虚增报价) 必需
竞价业务 价格信息实时叠加 当前最高价需与视频画面同步呈现,不能出现”视频显示旧价格但新价格已生效”的状态不一致 必需
信任增强 全程录制存证 拍卖全程的音视频和出价事件需要完整记录,用于争议回溯 必需
信任增强 CDN扩大受众 非出价的围观观众群需通过CDN旁路观看,降低RTC房间内的并发压力 必需
基础体验 音频高清降噪 拍卖师唱价、买家口语出价需要清晰无干扰,环境噪声需有效抑制 必需
竞价业务 分布式一致性 多个RTC边缘节点上,同一场拍卖的出价状态需全局一致 加分
信任增强 AI自动估价 基于历史拍卖数据实时给出参考估值区间 加分
信任增强 3D拍品建模 买家侧可旋转查看拍品三维模型,补充视频平面的局限 加分

2.2 需求深度解析

高分辨率与弱光处理是拍卖区别于其他直播场景的第一个分水岭。普通直播1080P足够”看个热闹”,但拍卖场景下,买家需要看清翡翠的絮状包体、字画的纸本纹理、瓷器的釉面开片等微观特征直接影响估价。此外,拍卖台上的灯光设计通常是有方向性的强聚光,暗部(器物的底部、背面转折处)如果没有弱光增强处理,画面就会呈现”亮部过曝、暗部死黑”的不可用状态。

出价消息的顺序一致性是分布式系统中最难的问题之一。考虑这个场景:买家A和买家B几乎同时出价,A的消息先到达服务器,B的消息后到达,但在某个买家的客户端上,因为网络抖动,B的出价先展示出来了。如果拍卖师依据的是服务器顺序喊价,而买家的屏幕显示的是不同顺序,信任瞬间崩塌。

SEI帧级叠加解决的是”价格信息与视频画面精确同步”的问题。传统做法是在网页上通过JavaScript更新一个DIV显示当前价格,但视频传输本身有缓冲和延迟,可能出现”视频中拍卖师还在展示一个青花碗的底部,价格已经跳到下一件拍品”的错位。将价格信息编码进视频帧的SEI中,可以保证价格信息只在对应视频帧被渲染时同时呈现。

三、RTC 技术选型

3.1 自建 vs 成熟SDK

直播拍卖系统选择自建RTC还是接入成熟SDK,核心不是”能不能做”,而是”做到什么程度才算能用”。

自建方案基于WebRTC开源栈,理论上的确可以把延迟压到很低。但要达到拍卖场景的可用标准,需要补的功课远超直觉预期:

  • WebRTC的默认QoS策略面向通用视频通话场景优化,对于拍卖的高分辨率+低延迟双重约束,需要深度调整拥塞控制算法和码率自适应策略
  • 弱网下的消息可靠有序传输需要自建信令通道的确认与重传机制
  • SEI的H.264编码层插入需要对编码器进行底层改造
  • 全球部署的边缘节点需要独立建设或对接CDN厂商
  • 云端录制、混流转码、CDN旁路推流这些”看起来是增值功能”的能力,在拍卖场景中全部是刚需

对比来看,成熟RTC SDK已经在以下维度上经过了大规模业务验证:

能力维度 自建WebRTC ZEGO Express SDK
端到端延迟 需深度调优,波动大 200ms,QoS自动适应
4K视频支持 依赖浏览器和设备 原生支持4K 60fps
SEI能力 需自行改造编码器 直接API调用 sendSEI
弱网QoS 基础丢包重传 分层编码+流量控制+自适应码率
云端录制 需自建 服务端API一键启动
CDN转推 需对接CDN addPublishCdnUrl直接转推
质量监控 星图全链路质量洞察
跨平台互通 有限 iOS/Android/Web/Windows/小程序/Linux全平台

3.2 关键技术指标

对于直播拍卖场景,以下指标需要明确打标:

  • 端到端视频延迟:< 300ms(RTC房间内)。拍卖师的动作和买家看到的画面之间延迟越低,买家的出价决策越接近现场体验
  • CDN观看延迟:< 1s(超低延迟直播模式)。围观观众也可以接受稍高延迟
  • 最大并发出价者:单房间50-100人(实际出价买家通常不超过此数量),围观者通过CDN分流
  • 视频分辨率:最低1080P,理想4K。码率根据分辨率自适应,4K推流建议8-15Mbps
  • 出价消息延迟:< 100ms(从发送到所有接收者收到)
  • 出价消息可靠性:100%到达,严格有序,不能丢失不能乱序
  • SEI发送频率:≤ 30次/秒(SDK限制),拍卖场景每500ms发送一次当前最高价即可
  • 录制时长:单场最长24小时(云端录制任务限制)

3.3 ZEGO Express SDK 能力匹配

对照上述需求,Express SDK中直接可用的能力:

  • 超低延迟:ZEGO自研QoS策略,200ms端到端延迟,弱网抗丢包30%
  • 高画质:最高4K 60fps,支持H.264硬件编码,自研z264编码器画质更高
  • 弱光增强setLowlightEnhancement 接口,AI视频引擎暗光增强
  • 超分辨率:自研AI视频引擎支持多倍超分
  • SEI发送sendSEI(streamID, Uint8Array) + playerRecvSEI回调接收
  • 可靠广播消息sendBroadcastMessage(10次/秒,1024字节,可靠)+ sendCustomCommand(可靠)
  • CDN转推addPublishCdnUrl 动态转推
  • 云端录制:通过服务端API启动,支持单流/混流录制和自动截图

四、系统架构设计

4.1 整体架构

系统分为三层:客户端层RTC网络层业务服务器层

┌─────────────────────────────────────────────────────────┐
│                    客户端层                              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐              │
│  │ 拍卖师端  │  │ 买家端   │  │ 观众端   │              │
│  │ (推流+SEI)│  │(拉流+出价)│  │(CDN观看) │              │
│  └─────┬─────┘  └─────┬────┘  └────┬─────┘              │
└────────┼──────────────┼─────────────┼────────────────────┘
         │              │             │
         ▼              ▼             ▼
┌─────────────────────────────────────────────────────────┐
│                   RTC 网络层 (ZEGO)                      │
│  ┌─────────────────┐  ┌──────────────┐                  │
│  │ Express 房间     │  │ ZIM 消息通道  │                 │
│  │ (音视频流+SEI)   │  │ (出价信令)    │                 │
│  └────────┬────────┘  └──────┬───────┘                 │
│           │                  │                           │
│  ┌────────┴──────────────────┴───────┐                  │
│  │        CDN 旁路转推               │                  │
│  │   (观众端 HLS/FLV 播放)           │                  │
│  └────────────────┬──────────────────┘                  │
│  ┌───────────────┴──────────────────┐                   │
│  │        云端录制服务               │                   │
│  │   (单流/混流录制 → 云存储)        │                   │
│  └──────────────────────────────────┘                   │
└─────────────────────────────────────────────────────────┘
         │              │
         ▼              ▼
┌─────────────────────────────────────────────────────────┐
│                  业务服务器层                             │
│  ┌──────────┐  ┌───────────┐  ┌────────────┐           │
│  │ 鉴权服务  │  │ 拍卖状态机 │  │ 录制管理    │           │
│  │(Token签发)│  │(出价校验+  │  │(启动/停止/  │           │
│  │           │  │ 顺序保障)  │  │ 查询任务)   │           │
│  └──────────┘  └─────┬─────┘  └────────────┘           │
│                      │                                   │
│               ┌──────┴──────┐                           │
│               │  出价日志存储 │                           │
│               │ (审计轨迹)   │                           │
│               └─────────────┘                           │
└─────────────────────────────────────────────────────────┘

客户端层三端职能不同:

  • 拍卖师端:推流高清视频 + 发送SEI(当前最高价信息)+ 通过ZIM发送状态控制消息(开始拍卖、结束拍卖、流拍等)
  • 买家端:拉流观看 + 接收SEI解析当前最高价 + 通过ZIM发送出价消息 + 在视频画面上叠加出价UI观众端:通过CDN观看(HLS/FLV),不进入RTC房间,不参与出价

RTC网络层是系统的核心传输管道:
– Express房间承载音视频流和SEI信息,延迟控制在200ms
– ZIM消息通道承载出价信令,使用可靠有序消息类型
– CDN旁路将流推向外部CDN,观众端以超低延迟直播(600-1000ms)观看
– 云端录制服务独立运行,不依赖任何客户端在线

业务服务器层独立于传输层,负责:
– 鉴权与Token签发(Express Token + ZIM Token)
– 拍卖状态机管理(预展→竞价→成交→流拍的状态转换)
– 出价顺序校验与冲突仲裁
– 录制任务的管理(通过服务端API启动、停止、查询云端录制)

4.2 数据流设计

数据类型 传输方式 通道 可靠性 频率 方向
拍卖师视频流 RTC推拉流 Express 尽力传输+QoS 持续 拍卖师 → 买家
拍品多角度视频 RTC推拉流 Express 尽力传输+QoS 持续 拍卖师 → 买家
当前最高价信息 SEI帧内嵌入 Express视频帧 随帧,可丢(频率多发补偿) 500ms/次 拍卖师 → 买家
买家出价指令 ZIMCustomMessage ZIM 可靠有序,100%到达 按需(出价时) 买家 → 服务端 → 所有买家
出价结果确认 ZIMCustomMessage ZIM 可靠有序 按需 服务端 → 所有买家+拍卖师
拍卖状态变更 sendBroadcastMessage Express房间 可靠广播 按需 拍卖师 → 所有买家
弹幕/聊天 ZIMBarrageMessage ZIM 不可靠 不限频 所有参与者
CDN观看流 RTMP/HLS/FLV CDN 标准直播 持续 ZEGO → CDN → 观众
录制文件 云端录制任务 云存储 持久化 ZEGO → OSS/S3

4.3 消息通道选型策略

这是拍卖系统架构设计中最关键的决策点。ZEGO生态中有两条消息通道:Express自带消息ZIM消息。选择哪一条承载出价指令,直接影响系统的一致性和可靠性。

消息通道 代表接口 可靠性 有序性 频率限制 消息大小 适用场景
Express 广播 sendBroadcastMessage 可靠 10次/秒 ≤1024B 拍卖状态通知
Express 自定义信令 sendCustomCommand 可靠 10次/秒(群发) ≤1024B 快速信令
ZIM 命令消息 ZIMCommandMessage 不可靠 不保证 30次/秒 ≤5KB 不适用出价
ZIM 自定义消息 ZIMCustomMessage 可靠有序 保证 10次/秒 ≤2KB(可扩32KB) 出价指令
ZIM 弹幕消息 ZIMBarrageMessage 不可靠 不保证 不限频 ≤5KB 观众讨论

选型结论

出价指令选择 ZIMCustomMessage。理由:
1. 可靠有序是刚需,出价不能丢、不能乱序
2. ZIM的消息有序性由服务端全局保证,不像Express自带消息仅保证客户端收发顺序
3. ZIM提供离线消息和消息漫游能力,可用于出价历史补全
4. 10次/秒的频率对于出价场景完全够用(同一个买家不可能每秒出价超过10次)

拍卖状态通知(开始/成交/流拍)使用 Express sendBroadcastMessage,因为它天然绑定RTC房间生命周期,拍卖师离开房间时这些通知自然失效。

观众弹幕互动使用 ZIMBarrageMessage,不限频、大并发、允许丢失但要求低延迟。

五、核心功能实现

5.1 高分辨率视频推流与弱光增强

拍卖师端需要以最高画质推流,同时开启弱光增强以保证暗部细节可见。

// ===== 拍卖师端:高分辨率推流 + 弱光增强 =====

import ZegoExpressEngine from 'zego-express-engine-webrtc';

const zg = new ZegoExpressEngine(zgConfig.appID, zgConfig.server);

// 1. 设置房间场景为高品质视频通话,SDK 会自动优化编码参数
zg.setRoomScenario(5); // 5 = HighQualityVideoCall,默认540p 25fps

// 2. 创建高分辨率视频流
// 拍卖场景建议:1080P起步,设备支持时使用4K
const stream = await zg.createZegoStream({
    camera: {
        video: {
            quality: 4,           // 4 = 自定义参数
            width: 1920,          // 1920x1080 (1080P)
            height: 1080,         // 也可以设为 3840x2160 (4K),取决于设备和带宽
            frameRate: 25,        // 25fps 拍品展示足够
            bitrate: 4000         // 4Mbps,4K 时建议 8-15Mbps
        },
        audio: {
            bitrate: 48           // 48kbps 音频码率
        }
    }
});

// 3. 开启弱光增强 —— 关键步骤
// 拍卖台通常使用定向聚光灯,暗部细节需要增强
zg.setLowlightEnhancement({
    mode: 1,         // 0=关闭,1=自动模式,2=手动模式
    level: 2         // 增强级别:0-3,级别越高增强越强
});

// 4. 登录房间并开始推流
await zg.loginRoom(roomID, token, { userID: auctioneerID, userName: '拍卖师' });
await zg.startPublishingStream(streamID, stream);

带宽与画质平衡的建议

直播拍卖的视频码率配置需要比普通直播更高,但不是越高越好。4K流在8-15Mbps码率下画质优秀,但需要买家端也具备相应的带宽和解码能力。建议策略:

  • 主拍卖画面:1080P/4Mbps,作为默认推流分辨率
  • 拍品特写镜头:如果使用双机位,特写机位推720P/2Mbps即可,节省上行带宽
  • 自适应降级:通过Express SDK的分层编码(SVC)能力,让弱网买家自动降级到低分辨率而不断流
  • 弱光增强不要开到最大——自动模式(mode=1)已经足够处理拍卖台的灯光环境,过度增强会导致亮部过曝

5.2 SEI 实时价格叠加

这是拍卖场景区别于其他直播的核心技术点。传统做法中,价格信息通过WebSocket或HTTP推送到客户端JavaScript层更新DOM,但这样无法保证价格与视频画面的精确同步。SEI将价格信息编码进H.264视频帧的用户自定义数据段,随帧传输、随帧渲染,天然同步。

// ===== 拍卖师端:发送 SEI 价格信息 =====

// 拍卖师端维护当前最高价状态
let currentAuctionState = {
    itemId: 'lot-2026-0042',
    currentPrice: 500000,       // 当前最高出价(分)
    bidderName: '买家003',
    bidIncrement: 50000,        // 加价幅度
    status: 'bidding'           // bidding | sold | passed
};

// SEI 配置:在初始化后设置
zg.setSEIConfig({
    SEIPass: true,              // 放开 SEI 收发限制
    // emulationPreventionByte: true  // 如果遇到NALU切片问题则开启
});

// 定时发送 SEI,频率 500ms/次
// SDK限制每秒不超过30次,500ms间隔安全
const seiInterval = setInterval(() => {
    // 将价格信息序列化为 JSON,再编码为 Uint8Array
    const priceData = JSON.stringify({
        p: currentAuctionState.currentPrice,    // 当前价
        b: currentAuctionState.bidderName,      // 出价人
        s: currentAuctionState.status,          // 状态
        i: currentAuctionState.itemId,          // 拍品ID
        t: Date.now()                           // 时间戳(用于延迟检测)
    });

    // 转为 Uint8Array,SEI 内容长度限制为 4096 字节
    const encoder = new TextEncoder();
    const seiBuffer = encoder.encode(priceData);

    // 发送 SEI
    const success = zg.sendSEI(streamID, seiBuffer);
    if (!success) {
        console.warn('SEI发送失败,可能超过频率限制');
    }
}, 500);

// 拍卖结束时清除定时器
function stopAuction() {
    clearInterval(seiInterval);
    currentAuctionState.status = 'sold';
    // 发送最终结果
    const encoder = new TextEncoder();
    zg.sendSEI(streamID, encoder.encode(JSON.stringify(currentAuctionState)));
}


// ===== 买家端:接收 SEI 并叠加价格信息 =====

// 监听 SEI 接收回调
zg.on('playerRecvSEI', (streamID, data) => {
    // data 是 Uint8Array,解码回 JSON
    const decoder = new TextDecoder();
    const jsonStr = decoder.decode(data);

    try {
        const auctionState = JSON.parse(jsonStr);

        // 计算 SEI 延迟(SEI中的时间戳 vs 当前时间)
        const latency = Date.now() - auctionState.t;

        // 更新UI中的价格显示
        updatePriceOverlay({
            currentPrice: auctionState.p,
            bidderName: auctionState.b,
            status: auctionState.s,
            itemId: auctionState.i,
            seiLatency: latency
        });

        // 如果延迟超过1秒,触发告警但不影响价格显示
        if (latency > 1000) {
            console.warn(`SEI延迟过高: ${latency}ms`);
        }
    } catch (e) {
        console.error('SEI数据解析失败:', e);
    }
});

// 坐落到DOM上的价格叠加层
function updatePriceOverlay(state) {
    // 使用价格格式化(分 → 元,保留万位逗号分隔)
    const formattedPrice = (state.currentPrice / 100)
        .toLocaleString('zh-CN', { style: 'currency', currency: 'CNY' });

    document.getElementById('price-overlay').innerHTML = `
        <div class="current-price">${formattedPrice}</div>
        <div class="bidder-info">当前出价:${state.bidderName}</div>
        <div class="auction-status ${state.status}">
            ${state.status === 'bidding' ? '竞价中' :
              state.status === 'sold' ? '已成交' : '已流拍'}
        </div>
    `;
}

SEI方案相比WebSocket推送的优点
– 价格信息和视频帧来自同一时间源,不会出现”新价格配旧画面”的错位
– 不需要额外的信令连接,减少客户端复杂度
– CDN观看的观众端也能通过解析FLV/HLS中的SEI来获取价格(需要支持SEI解析的播放器)

注意事项
– SEI跟随视频帧,视频帧丢失时SEI也丢失,因此在频率限制内(30次/秒)应该多发几次(500ms间隔已经是每12-15帧发一次)
– H.264编码下SEI的NAL unit type为6,payload type为5
– CDN播放端如果要读取SEI,需要使用支持SEI解析的播放器(flv.js默认不支持,需要在demux层做自定义处理)

5.3 出价信令的可靠有序传输

出价指令是拍卖系统的命脉。这里使用ZIM的ZIMCustomMessage,该消息类型提供可靠有序的传输保证。

// ===== ZIM 初始化与消息通道建立 =====

import ZIM from 'zego-zim-web';

// 创建 ZIM 实例
const zim = ZIM.create({
    appID: zgConfig.appID,
    serverURL: zimConfig.serverURL  // ZIM 的服务器地址,注意不是 Express 地址
});

// 登录 ZIM(使用与Express相同的userID,方便关联)
await zim.login({
    userID: buyerID,
    userName: `藏家_${buyerID}`,
    token: zimToken     // ZIM 的 Token,由业务服务器签发
}, ZIMConversationType.Room);

// 加入拍卖房间(ZIM房间与Express房间是独立的)
await zim.joinRoom(auctionRoomID);


// ===== 买家端:发送出价指令 =====

/**
 * 发送出价指令
 * @param {number} amount - 出价金额(分)
 * @param {string} itemId - 拍品ID
 */
async function placeBid(amount, itemId) {
    // 构造出价消息体
    const bidMessage = {
        type: 'bid',              // 消息类型标识
        itemId: itemId,           // 拍品ID
        amount: amount,           // 出价金额
        bidderId: buyerID,        // 出价人ID
        timestamp: Date.now(),    // 客户端时间戳
        nonce: generateNonce()    // 幂等键,防重复
    };

    // 使用 ZIMCustomMessage 发送可靠有序消息
    const customMessage = new ZIMCustomMessage();
    customMessage.message = JSON.stringify(bidMessage);
    // subType 用于在接收端区分消息类型
    customMessage.subType = 100;  // 100 = 出价指令

    // 发送配置
    const config = {
        priority: 1,  // 1=高优先级,确保出价消息优先投递
        // pushConfig 可配置离线推送(拍卖场景通常不需要)
    };

    try {
        const result = await zim.sendMessage(
            customMessage,
            auctionRoomID,
            ZIMConversationType.Room,
            config
        );

        if (result.message) {
            console.log('出价已发送,消息ID:', result.message.messageID);
            // 本地先乐观更新UI(显示"出价中"状态)
            updateBidStatus('pending');
        }
    } catch (error) {
        console.error('出价发送失败:', error);
        // 网络异常时给用户明确提示
        showErrorToast('出价未送达,请检查网络后重试');
    }
}

// 生成幂等键(防止弱网重试导致重复出价)
function generateNonce() {
    return `${buyerID}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}


// ===== 买家端:接收出价结果和他人出价 =====

// 监听 ZIM 消息接收事件
zim.on('messageReceived', (messageList, fromConversationID, type) => {
    messageList.forEach(msg => {
        // 只关注自定义消息
        if (msg.type !== 200) return; // 200 = ZIMCustomMessage

        try {
            const data = JSON.parse(msg.message);

            // 根据 subType 区分消息类型
            switch (msg.subType) {
                case 100: // 出价指令(来自其他买家)
                    handleBidUpdate(data, msg);
                    break;

                case 101: // 出价确认(来自服务端)
                    handleBidConfirm(data, msg);
                    break;

                case 102: // 拍卖状态变更
                    handleAuctionStateChange(data, msg);
                    break;
            }
        } catch (e) {
            console.error('消息解析失败:', e);
        }
    });
});

// 处理他人的出价更新
function handleBidUpdate(data, msg) {
    // 更新UI中的当前最高价
    // 注意:这里收到的已是服务端确认后的有序消息
    updateCurrentPrice(data.amount, data.bidderId);

    // 记录到出价历史
    appendBidHistory({
        amount: data.amount,
        bidderId: data.bidderId,
        time: msg.timestamp,
        messageId: msg.messageID
    });
}

// 处理服务端的出价确认
function handleBidConfirm(data, msg) {
    if (data.bidderId === buyerID) {
        if (data.accepted) {
            updateBidStatus('accepted');  // 出价被接受
            playBidAcceptedSound();
        } else {
            updateBidStatus('rejected');  // 出价被拒绝(加价不足/超出上限等)
            showErrorToast(data.reason || '出价无效');
        }
    }
}

为什么不用Express自带的sendBroadcastMessage

Express的sendBroadcastMessage虽然也是”可靠”的,但它的可靠性是客户端层面的,保证消息从发送者到ZEGO服务器,以及ZEGO服务器向房间内的其他用户广播。ZIM的可靠有序保证是在服务端全局排序的,当多个买家并发出价时,ZIM服务端会为每条消息分配全局有序的消息ID,保证所有接收端看到的顺序完全一致。这是分布式竞价系统的基础保障。

5.4 云端录制与存证

云端录制的启动是通过ZEGO服务端API完成的,不是在客户端SDK中调用。以下是服务端启动录制任务的实现。

// ===== 业务服务器端:启动云端录制 =====
// 注意:以下代码在业务服务器上运行,使用 Node.js

const crypto = require('crypto');

/**
 * 生成 ZEGO 服务端 API 签名
 * 签名算法:https://doc-zh.zego.im/article/14555
 */
function generateSignature(appId, secret, timestamp, nonce) {
    const str = `${appId}${timestamp}${nonce}${secret}`;
    return crypto.createHash('sha256').update(str).digest('hex');
}

/**
 * 启动云端混流录制
 * 将拍卖师画面+出价信息叠加一起录制
 */
async function startCloudRecording(roomID, auctionInfo) {
    const appId = process.env.ZEGO_APP_ID;
    const secret = process.env.ZEGO_APP_SECRET;

    // 公共参数
    const timestamp = Math.floor(Date.now() / 1000);
    const nonce = Math.random().toString(36).substr(2, 10);
    const signature = generateSignature(appId, secret, timestamp, nonce);

    // StartRecord 请求体
    const requestBody = {
        RoomId: roomID,
        RecordInputParams: {
            RecordMode: 2,          // 2 = 混流录制(拍卖需要单文件完整记录)
            StreamType: 3,          // 3 = 录制音视频(合并)
            MaxIdleTime: 120,       // 房间内无人时最长空闲时间(秒),超时自动停止
            MixConfig: {
                MixMode: 2,         // 2 = 预设布局模式
                MixOutputStreamId: `mix_${roomID}`,
                MixOutputVideoConfig: {
                    Width: 1920,    // 录制分辨率1080P
                    Height: 1080,
                    Fps: 25,
                    Bitrate: 3000000 // 3Mbps录制码率
                }
            }
        },
        RecordOutputParams: {
            OutputFileFormat: 'mp4',
            // 文件名模板:拍卖ID_日期_时间
            OutputFolder: `auctions/${auctionInfo.id}/${timestamp}/`
        },
        StorageParams: {
            Vendor: 2,              // 2 = 阿里云OSS(0=AWS S3, 1=腾讯云COS)
            Region: process.env.OSS_REGION,
            Bucket: process.env.OSS_BUCKET,
            AccessKeyId: process.env.OSS_ACCESS_KEY_ID,
            AccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET
        }
    };

    const response = await fetch(
        `https://cloud-recording-api.zego.im/v2/start_record?AppId=${appId}&Signature=${signature}&SignatureNonce=${nonce}&SignatureVersion=2.0&Timestamp=${timestamp}`,
        {
            method: 'POST',
            headers: { 'Content-Type': 'application/json;charset=utf-8' },
            body: JSON.stringify(requestBody)
        }
    );

    const result = await response.json();

    if (result.ErrorCode === 0) {
        console.log('录制任务已启动, TaskId:', result.TaskId);
        // 存储 TaskId,用于后续查询状态和停止录制
        await saveRecordingTaskId(auctionInfo.id, result.TaskId);
        return result.TaskId;
    } else {
        console.error('启动录制失败:', result.ErrorMessage);
        throw new Error(`录制启动失败: ${result.ErrorMessage}`);
    }
}

/**
 * 停止云端录制
 */
async function stopCloudRecording(taskId) {
    const appId = process.env.ZEGO_APP_ID;
    const secret = process.env.ZEGO_APP_SECRET;

    const timestamp = Math.floor(Date.now() / 1000);
    const nonce = Math.random().toString(36).substr(2, 10);
    const signature = generateSignature(appId, secret, timestamp, nonce);

    const requestBody = { TaskId: taskId };

    const response = await fetch(
        `https://cloud-recording-api.zego.im/v2/stop_record?AppId=${appId}&Signature=${signature}&SignatureNonce=${nonce}&SignatureVersion=2.0&Timestamp=${timestamp}`,
        {
            method: 'POST',
            headers: { 'Content-Type': 'application/json;charset=utf-8' },
            body: JSON.stringify(requestBody)
        }
    );

    return await response.json();
}

/**
 * 查询录制状态和文件列表
 */
async function queryRecordingStatus(taskId) {
    const appId = process.env.ZEGO_APP_ID;
    const secret = process.env.ZEGO_APP_SECRET;

    const timestamp = Math.floor(Date.now() / 1000);
    const nonce = Math.random().toString(36).substr(2, 10);
    const signature = generateSignature(appId, secret, timestamp, nonce);

    const requestBody = { TaskId: taskId };

    const response = await fetch(
        `https://cloud-recording-api.zego.im/v2/describe_record_status?AppId=${appId}&Signature=${signature}&SignatureNonce=${nonce}&SignatureVersion=2.0&Timestamp=${timestamp}`,
        {
            method: 'POST',
            headers: { 'Content-Type': 'application/json;charset=utf-8' },
            body: JSON.stringify(requestBody)
        }
    );

    return await response.json();
}

录制策略建议

对于拍卖场景,推荐使用混流录制模式(RecordMode=2),将拍卖师视频、拍品特写和出价信息叠加画面一并录制成单个MP4文件。这样做的好处:
– 一个文件包含了完整的拍卖上下文,审计时不需要拼接多个文件
– 可以通过MixConfig精确控制录制画面的布局和分辨率
– 结合录制状态回调,服务端可以实时感知录制进度

录制过程中,建议同时开启自动截图(设置OutputFileFormat为”jpg”),每10秒截图一次,作为出价时刻的视觉存档。

5.5 CDN 旁路推流

RTC房间内的观众数受限于并发带宽和边缘节点容量。拍卖的出价买家通常只有几十人,但围观群众可能有上万人。通过CDN旁路推流,将拍卖画面推向CDN,万人围观不占用RTC房间资源。

// ===== 拍卖师端或服务端:启动 CDN 转推 =====

/**
 * 将拍卖画面转推到 CDN
 * 观众通过 HLS 或 FLV 观看
 */
async function startCDNPush(streamID) {
    // 推流域名在 ZEGO 控制台配置
    const cdnURL = 'rtmp://push.example.com/live/';

    try {
        // addPublishCdnUrl 会将当前的RTC流转推到指定的CDN推流地址
        const result = await zg.addPublishCdnUrl(
            streamID,
            `${cdnURL}${streamID}` // CDN推流完整URL
        );

        console.log('CDN转推已启动:', result);
    } catch (error) {
        console.error('CDN转推失败:', error);
    }
}

// 停止CDN转推
async function stopCDNPush(streamID) {
    const cdnURL = 'rtmp://push.example.com/live/';
    await zg.removePublishCdnUrl(streamID, `${cdnURL}${streamID}`);
}


// ===== 观众端:CDN 播放(使用 flv.js 示例) =====

import flvjs from 'flv.js';

function createCDNPlayer(streamID) {
    // CDN 拉流地址(根据CDN厂商的播放域名配置)
    const playURL = `https://play.example.com/live/${streamID}.flv`;

    const player = flvjs.createPlayer({
        type: 'flv',
        url: playURL,
        isLive: true,
        hasAudio: true,
        hasVideo: true,
        // 如果需要在CDN播放器中也显示价格信息,
        // 需要自定义demux层解析SEI(flv.js默认不支持SEI解析)
        // customDemuxer: SEIAwareDemuxer
    });

    player.attachMediaElement(document.getElementById('cdn-video'));
    player.load();
    player.play();

    return player;
}

CDN推流对SEI的影响:通过CDN播放时,SEI信息不会被标准的flv.js/hls.js解析,CDN观众将无法看到SEI中携带的价格信息。解决方案有两种思路:
方案一(推荐):CDN观众的UI上通过独立的WebSocket连接获取价格信息,价格更新延迟可能比RTC房间内略高(约1-2秒),但对围观观众来说无伤大雅
方案二:自定义flv.js的demux层,从FLV Tag中提取SEI数据。这需要深入理解FLV协议和H.264 Annex B格式,工作量较大

六、关键问题与优化策略

问题 根因分析 优化策略
出价消息乱序 多个买家并发出价+网络路径差异,消息到达服务端的物理顺序不等于发送顺序 1. 使用ZIMCustomMessage的可靠有序保证,ZIM服务端为每条消息分配全局递增序列号,所有接收端按序列号排序展示;2. 服务端维护拍卖状态机的出价队列,按服务端时间戳排序仲裁;3. 客户端收到的出价消息先入本地有序队列再渲染,不要直接按到达顺序渲染
弱网下出价丢失 买家网络抖动导致TCP/UDP包丢失,或者应用层超时未收到ACK 1. ZIMCustomMessage底层有自动重传机制,应用层无需额外处理;2. 客户端发送出价后,启动3秒超时计时器,未收到服务端确认则自动重试(携带相同的nonce幂等键避免重复扣款);3. 出价UI层面做”发送中→已送达→已确认”三态切换,让用户明确感知消息状态
SEI信息丢失导致价格不同步 SEI跟随视频帧,网络丢包会导致视频帧丢失,SEI随之丢失 1. 提高SEI发送频率:500ms/次(每秒2次),即便丢一帧也会在500ms内补上;2. 买方客户端同时通过ZIM接收一份价格快照作为降级方案(服务端每次确认出价后广播当前最高价);3. 客户端展示价格时优先使用SEI(延迟最低),SEI超过1.5秒未更新则回退到ZIM价格
4K视频带宽不足 上行或下行带宽无法支撑4K码率,导致卡顿或自动降级为低分辨率 1. 推流端开启分层编码(SVC),服务器根据每个接收端的带宽情况自动下发合适的分辨率层;2. 带宽富足的买家收4K,带宽紧张的收720P,各自体验最优;3. 拍卖师端建议使用有线网络,上行带宽不低于20Mbps(4K需要8-15Mbps + 余量)
录制文件缺失导致审计不完整 录制任务意外中断、云存储上传失败、录制回调丢失 1. 同时启动单流录制+混流录制两路冗余;2. 监听录制状态回调(包括异常状态),异常时自动重启录制任务;3. 录制文件上传完成后校验文件大小和时长,异常时触发告警;4. 本地同时做一份推流端的本地录制作为最终兜底
分布式一致性 全球多区域部署时,不同边缘节点的拍卖状态可能出现短暂不一致 1. 拍卖状态机只在一台主节点上运行(单主模式),所有出价请求路由到主节点处理;2. ZIM的消息有序性跨区域生效(消息通过ZEGO全球MSDN网络同步);3. 客户端重连时通过业务服务器的”当前状态同步”接口补全状态,不依赖消息回放

6.1 出价消息的幂等性设计(深度展开)

出价消息的”不丢失不重复”比一般即时通讯场景严格得多。ZIMCustomMessage保证了传输层的可靠有序,但应用层仍需处理边界情况:

买家端发送出价 → ZIM服务器确认投递 → 服务端处理出价 → 服务端返回出价确认

如果在"服务端返回出价确认"环节网络中断:
- 买家端不知道出价是否被接受
- 如果买家重试,服务端需要能识别这是重试还是新出价

解决策略:nonce幂等键 + 服务端去重窗口

// 出价请求结构中的幂等键
const bidMessage = {
    type: 'bid',
    itemId: 'lot-2026-0042',
    amount: 550000,
    bidderId: 'buyer_003',
    nonce: 'buyer_003_1717526400000_a3kf92m4d', // 唯一幂等键
    timestamp: Date.now()
};

// 服务端处理逻辑(伪代码)
function processBid(message) {
    // 1. 检查 nonce 是否已经处理过(窗口期:最近60秒内的nonce)
    if (redis.exists(`bid_nonce:${message.nonce}`)) {
        // 已处理过,直接返回之前的结果(幂等响应)
        return redis.get(`bid_result:${message.nonce}`);
    }

    // 2. 校验出价合法性
    if (!validateBid(message.itemId, message.amount, message.bidderId)) {
        const result = { accepted: false, reason: 'invalid_bid' };
        // 记录nonce防止重复处理
        redis.setex(`bid_nonce:${message.nonce}`, 60, JSON.stringify(result));
        return result;
    }

    // 3. 更新拍卖状态
    const result = updateAuctionState(message);

    // 4. 记录nonce和结果
    redis.setex(`bid_nonce:${message.nonce}`, 60, 'processed');
    redis.setex(`bid_result:${message.nonce}`, 60, JSON.stringify(result));

    return result;
}

七、场景延伸与扩展玩法

7.1 AI自动估价辅助

在拍卖师展示拍品的同时,AI模型对视频流中的拍品进行实时视觉分析,结合历史拍卖数据库给出参考估值区间。

技术实现思路:
– 通过Express的自定义视频采集能力,将摄像头画面同时喂给RTC推流和一个AI推理服务
– AI服务对拍品类型分类(瓷器/字画/玉器/珠宝等),提取视觉特征(釉色、笔触、透明度等),比对历史拍卖数据库
– 通过ZIMCustomMessage将AI估价结果推送到拍卖师端和买家端
– 估价结果仅作参考,不影响实际竞价流程

7.2 3D拍品建模与AR预览

弥补2D视频的视角局限,买家用手机摄像头扫描桌面,将拍品的3D模型以AR方式”摆放”在自己的空间里,旋转查看各个角度。

技术实现思路:
– 拍卖前使用3D扫描设备(或手机photogrammetry算法)生成拍品的GLB/USDZ格式模型
– 模型文件通过CDN分发,买家端在拍卖前预加载
– 买家端使用WebXR API或ARKit/ARCore将模型渲染到现实场景中
– 模型的旋转角度与拍卖师视频中的展示节奏可以通过ZIM消息同步——当拍卖师说”请看底部”,模型同时自动旋转到底面视角

7.3 跨境多语言同步拍卖

同一场拍卖,北京、伦敦、纽约三地的买家同时参与,各自听到母语(或看到母语字幕)的拍卖师唱价和拍品解说。

技术实现思路:
– 利用Express的多房间推流能力,主拍卖师推一条原始音视频流
– 在各地部署AI同声传译服务,实时生成当地语言的音频轨道和文字字幕
– 通过SEI发送多语言字幕文本,客户端根据语言偏好选择展示
– ZIM的全球MSDN网络保证跨区域的出价消息延迟一致

7.4 区块链存证与数字藏品

将拍卖全程的关键事件(开始拍卖、每次出价、成交确认)写入区块链,生成不可篡改的拍卖凭证。高价值成交拍品还可同步铸造数字藏品(NFT)给买家。

技术实现思路:
– 服务端在拍卖状态机的关键节点生成Hash(包含前一区块Hash + 事件数据 + 时间戳)
– 通过智能合约将Hash锚定到链上(选择低Gas费的L2方案或联盟链)
– 云端录制文件的SHA-256 Hash同样上链,形成”录制文件 <-> 链上Hash”的可验证关联
– 数字藏品铸造通过与ZEGO服务端API联动:成交瞬间自动截帧、调用合约Mint

7.5 虚拟拍卖厅

用3D引擎(Unity/Unreal)构建沉浸式虚拟拍卖厅,买家以Avatar形式入座,拍卖师在虚拟台上展示3D拍品。这是”3D拍品建模”的高级形态。

技术实现思路:
– Express SDK集成到Unity/Unreal中(ZEGO提供C++ SDK和Unity SDK)
– 虚拟拍卖厅中的Avatar动作和状态通过ZIM的可靠有序消息同步
– Express的范围音频(Range Audio)能力让买家感知到空间中其他人的声音方向和距离——坐在左边的买家出价时,声音从左边传来
– 拍卖师端的动作捕捉(通过摄像头或VR手柄)实时驱动虚拟拍卖师Avatar的动作

八、总结

直播拍卖不是”秀场直播加一个出价按钮”那么简单。它与传统直播的根本区别在于:信息不仅要传得快,还要传得对、传得可追溯——出价不能丢、不能乱、不能事后说不清。

技术选型上,全链路接入成熟RTC方案而非自建,是最务实的选择。拍卖业务的核心竞争力在拍品资源和买家关系,不在音视频引擎的底层优化。ZEGO Express SDK解决了延迟和画质的工程问题,ZIM SDK解决了出价消息的可靠有序问题,云端录制解决了审计存证问题——这三者构成了直播拍卖的完整技术底座。

核心要点归纳:

  1. 画质是信任的基础:4K分辨率+弱光增强+SEI帧级价格叠加,让远程买家获得接近现场的判断依据
  2. 有序是拍卖的生命线:ZIMCustomMessage的可靠有序保证是出价系统的底层依赖,Express自带的广播消息不能满足并发出价的顺序一致性需求
  3. 存证是事后裁决的依据:混流录制+自动截图+出价日志形成三层审计体系
  4. 架构的分层隔离是可扩展性的保障:RTC房间承载核心竞价买家,CDN旁路承载围观群众,ZIM承载出价信令。三通道物理隔离,互不干扰。

技术选型一句话:Express SDK提供延迟<200ms的音视频通道,ZIM SDK提供可靠有序的消息通道,云端录制API提供完整的审计存证。三者组合,即可构建一套可投入生产的直播拍卖系统。

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

(0)

相关推荐