一、引言
一块和田玉籽料在拍卖师的掌心缓缓翻转,强光手电扫过,脂白色的玉质泛出温润的油光。屏幕另一端,六位藏家同时注意到了皮壳上那处细微的洒金——四位立刻举牌,各自加价。
这不是预录视频,这是一场实时直播拍卖。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解决了出价消息的可靠有序问题,云端录制解决了审计存证问题——这三者构成了直播拍卖的完整技术底座。
核心要点归纳:
- 画质是信任的基础:4K分辨率+弱光增强+SEI帧级价格叠加,让远程买家获得接近现场的判断依据
- 有序是拍卖的生命线:ZIMCustomMessage的可靠有序保证是出价系统的底层依赖,Express自带的广播消息不能满足并发出价的顺序一致性需求
- 存证是事后裁决的依据:混流录制+自动截图+出价日志形成三层审计体系
- 架构的分层隔离是可扩展性的保障:RTC房间承载核心竞价买家,CDN旁路承载围观群众,ZIM承载出价信令。三通道物理隔离,互不干扰。
技术选型一句话:Express SDK提供延迟<200ms的音视频通道,ZIM SDK提供可靠有序的消息通道,云端录制API提供完整的审计存证。三者组合,即可构建一套可投入生产的直播拍卖系统。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/info/67357.html