一、引言
这个色号显白还是显黑?你帮我看看这个面料质感怎么样?链接发你了,点进去直接看……电商购物决策链路中,这类闺蜜间的实时求助几乎是日常。但在传统电商架构下,这个链路是断裂的:截图发微信、复制链接、打字描述、等对方回复,每一跳都在消耗注意力,等一圈反馈下来,购物冲动已经凉了。
更不用说直播间场景。两个闺蜜在不同的直播间或同一间直播间里想同步讨论,只能靠文字弹幕或切到微信语音,一边盯屏幕一边分心通话,体验极其割裂。
一起购物(Co-Shopping)的本质需求是:两个或多个用户在同一时刻看到同一件商品,实时讨论,共同决策。文字聊天的延迟感、截图分享的异步感,都和“一起逛”的心理预期相悖。唯有实时音视频 + 屏幕/页面同步的组合,才能真正把逛商场的体验搬到线上。
本文将拆解一起购物场景的完整技术方案:从需求分析到架构设计,从屏幕共享到购物车同步,再到弱网优化和隐私保护。并提供基于ZEGO RTC SDK 实现实时互动购物功能示例代码。
二、场景技术需求拆解
一起购物不是简单的视频通话 + 电商页面,它的技术需求涉及音视频通道、数据同步通道、业务状态通道三条线的深度协作。
2.1 需求全景
| 序号 | 功能需求 | 需求细节 | 优先级 |
|---|---|---|---|
| F1 | 1v1/多人音视频通话 | 闺蜜间实时语音讨论,可选视频画面看对方表情和反应 | 必需 |
| F2 | 屏幕/页面同步浏览 | 一方浏览商品详情页时,另一边同步看到相同页面内容(非镜像屏幕,而是商品页面的 URL + 滚动位置同步) | 必需 |
| F3 | 购物车实时同步 | 任一方添加/删除商品、修改数量、添加收藏,另一方即时看到变化 | 必需 |
| F4 | 商品卡片消息 | 在通话中发送结构化的商品信息卡片(商品图、价格、标题),点击直达详情页 | 必需 |
| F5 | 通话云端录制 | 录制购物决策过程(讨论→比较→下单),支持回放和分享 | 加分 |
| F6 | 虚拟背景/人像抠图 | 购物场景下用户对形象有一定要求,不希望杂乱背景分散注意力 | 加分 |
| F7 | AI 购物助手 | 实时语音识别讨论内容,提供比价、评价摘要、相似款推荐 | 加分 |
2.2 各需求的技术细节
F1 音视频通话:一起购物场景的核心交互。语音的延迟要求 < 200ms——「这件好看不」问出去如果半秒后才听到回应,互动感就断档了。视频不是必须的,但看到闺蜜拧眉的表情往往就是「这件不行」的最快信号。视频码率可低至 150-300 kbps,画质优先级低于语音。
F2 屏幕/页面同步:这里有两种方案路径。方案 A 是真正的屏幕共享(Screen Share)——把一方手机屏幕的浏览器内容直接编码推流给另一方;方案 B 是 URL + 滚动位置的实时同步——双方各自在本地加载同一个商品详情页,仅同步导航行为和视角位置。方案 A 的优点是所有内容都能共享(包括不可分享的页面状态),缺点是隐私风险和带宽开销;方案 B 省带宽但要求双方都能访问同一页面。实际场景中推荐方案 A(屏幕共享)为主 + 方案 B(URL 同步)为辅的组合。
F3 购物车同步:这是一个分布式状态一致性问题。双方各自维护一份购物车副本,当一方执行写操作(添加/删除/修改数量)时,通过可靠消息通道同步到另一方。需要考虑并发写入冲突——如果两方同时修改同一个商品的数量,需要冲突解决策略。
F4 商品卡片消息:结构化消息,不同于普通文本聊天。卡片包含商品标题、缩略图、价格、来源平台等字段。通过 ZIM CustomMessage 承载,subType 区分普通聊天和商品卡片。
F5 云端录制:可选择混流录制(将屏幕共享画面 + 语音合成一个 MP4 文件),用于回看购物决策全过程。类似「逛商场 vlog」,也可以分享到社交平台。
F6 虚拟背景:Express SDK 内置的虚拟背景能力,基于人像分割将用户从杂乱背景中抠出,替换为统一背景。对女性用户为主的购物场景,这个功能的体感价值很高。
F7 AI 购物助手:利用实时语音识别(ASR)将双方对话转写为文本,NLP 提取商品关键词,向大模型查询比价、评价情绪分析、相似商品推荐,结果以卡片消息形式推送到通话界面。
三、RTC 技术选型
3.1 自建 vs 第三方 RTC SDK
一起购物场景涉及媒体流传输、消息通道、屏幕共享三条技术线。自建方案需要分别解决 WebRTC 信令服务、TURN/STUN 穿透、消息中间件、屏幕采集适配等问题。
| 维度 | 自建方案(WebRTC + WebSocket) | 成熟 RTC SDK |
|---|---|---|
| 音视频延迟 | 依赖自建 SFU 质量和网络调度,通常 300-500ms | 专属加速网络,音频 < 200ms |
| 屏幕共享 | 浏览器原生 getDisplayMedia,适配和兼容需自行处理 |
SDK 封装,统一 API 跨 Chrome/Firefox/Safari/Edge |
| 消息同步 | WebSocket 自建,可靠性和有序性需自行保证 | 内置消息通道 + ZIM,区分可靠/不可靠通道 |
| 弱网对抗 | 无 FEC/SVC,丢包时画质和音频断崖式下降 | FEC + 自适应码率 + QoS,弱网也不断流 |
| 3A 处理 | 需自研或集成第三方音频处理库 | SDK 内置 AEC/AGC/ANS + AI 降噪 |
| 云端录制 | 需要搭单独的录制服务 | 服务端 API 直接启动,支持混流/单流/截图 |
| 跨平台 | 各端 WebRTC 实现有差异,iOS 限制尤其多 | 一套 SDK 多端接口统一 |
结论:一起购物对同步的实时性、可靠性和隐私保护都有要求,自建方案在消息有序同步和弱网对抗方面工程量巨大。成熟的第三方 RTC 引擎是合理选择。
3.2 关键技术指标
以一起购物为目标场景,选择 RTC 引擎的核心指标:
- 音频端到端延迟:< 200ms。闺蜜间的「好看吗?」需要即时反馈
- 屏幕共享延迟:< 500ms。页面滑动后对方侧延迟过大则无法同步讨论焦点
- 弱网抗丢包率:30% 丢包下音频仍可流畅沟通,视频可适度降级
- 消息通道延迟:< 100ms。购物车的增删操作需要即刻同步到对方
- 并发连接数:支持 2-6 人同时在线(多人一起逛需求)
- 跨平台:iOS / Android / Web / 小程序四端互通,购物场景以移动端为主
- 屏幕共享的隐私控制:分享者端能控制分享范围和内容可见性
3.3 以 ZEGO Express SDK 为参考的能力匹配
即构科技的实时音视频 SDK(ZEGO Express SDK) 是一套覆盖 RTC 音视频、直播、屏幕共享、消息通道的实时互动基础设施。其核心能力与一起购物场景的匹配度如下:
| Express SDK 能力 | 在一起购物中对应 | 关键参数 |
|---|---|---|
| 低延迟音视频通话 | 闺蜜/多人实时语音讨论 + 视频看表情 | 音频 < 200ms,视频可降到 180p/15fps |
| 屏幕共享 | 一方浏览商品详情页,另一方实时同步观看 | 支持浏览器标签页/应用窗口/整个屏幕 |
| sendBroadcastMessage | 同步购物车状态变更(可靠、有序) | 10 条/秒,≤ 1024 字节 |
| sendCustomCommand | 同步页面 URL 和滚动位置(高频、低延迟) | 30 条/秒,不可靠 |
| 3A + AI 降噪 | 嘈杂环境(咖啡厅、户外)也能清晰通话 | AI 降噪处理稳态噪声 |
| 虚拟背景 | 居家购物时的背景隐私保护 | 人像分割 + 背景替换 |
| 云端录制 | 录制购物决策过程 | 混流/单流录制,第三方云存储 |
| CDN 旁路推流 | 扩展为「达人陪逛」直播模式 | 转推 CDN,支撑大量并发观看 |
| 星图质量监控 | 线上问题排查、通话质量追踪 | 全链路 QoS 数据 |
消息通道方面,即构科技即时通讯 SDK(ZIM SDK) 提供更丰富的消息类型:一起购物场景中需要区分高频信令(页面同步)和可靠消息(购物车操作),恰好对应 ZIM Command Message(不可靠、30 条/秒、≤ 5KB)和 ZIM Custom Message(可靠有序、10 条/秒)的组合。
四、系统架构设计
4.1 整体架构
一起购物系统的架构分为三层:客户端层、传输通道层和业务服务层。
┌─────────────────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ ┌────────────────────────────┐│
│ │ 购物 UI 层 │ │ Express SDK │ │ ZIM SDK ││
│ │ - 商品详情页 │ │ - 音视频通话 │ │ - 购物车状态同步 ││
│ │ - 购物车面板 │ │ - 屏幕共享 │ │ - 商品卡片消息 ││
│ │ - 商品卡片组件 │ │ - 虚拟背景 │ │ - URL/滚动位置同步 ││
│ │ - 语音控制条 │ │ - 通话质量监控 │ │ - 房间管理 ││
│ └────────┬────────┘ └────────┬─────────┘ └─────────────┬──────────────┘│
│ │ │ │ │
└───────────┼────────────────────┼───────────────────────────┼────────────────┘
│ │ │
┌───────▼────────┐ ┌───────▼──────────┐ ┌───────────▼──────────┐
│ 业务服务层 │ │ RTC 传输层 │ │ ZIM 消息服务 │
│ │ │ (ZEGO 全球加速网络)│ │ │
│ - 用户鉴权 │ │ │ │ - 房间消息路由 │
│ - Token 签发 │ │ - 音视频流分发 │ │ - 消息持久化 │
│ - 商品信息查询 │ │ - 屏幕共享流分发 │ │ - 有序可靠投递 │
│ - 云端录制调度 │ │ - 媒体信令中转 │ │ - 离线消息 │
│ - 购物车存储 │ │ - FEC/弱网优化 │ │ - 已读回执 │
│ - AI 推荐服务 │ └──────────────────┘ └──────────────────────────┘
└────────────────┘
核心设计决策:
- 音视频与消息分离:Express SDK 负责媒体传输(低延迟音视频 + 屏幕共享),ZIM SDK 负责结构化消息通道(购物车同步 + 卡片消息 + URL 同步)。二者在客户端并行运行,各走自己的网络通道,避免信令数据干扰媒体传输质量。
- 业务服务器轻量化:Token 签发、商品信息查询、购物车持久化放在业务服务端。房间管理和媒体转发完全由 Express/ZIM 服务端处理,不经过业务服务器——这避免了业务服务器成为媒体流的瓶颈。
- 隐私边界在客户端:屏幕共享的隐私过滤逻辑在客户端执行——在浏览器层面判断哪些内容允许被共享、哪些窗口需要屏蔽,不依赖服务端处理。
4.2 数据流设计
一起购物场景涉及五类核心数据流,各自对传输通道有不同要求:
| 数据类型 | 示例内容 | 传输通道 | 可靠性 | 频率 | 延迟要求 |
|---|---|---|---|---|---|
| 语音/视频流 | 实时对话音频、人脸视频 | Express 媒体通道 | 允许少量丢包 | 持续 | < 200ms |
| 屏幕共享流 | 商品详情页画面 | Express 媒体通道 | 允许少量丢包 | 持续 | < 500ms |
| 页面同步指令 | URL 变更、滚动位置、Tab 切换 | ZIM Command Message 或 sendCustomCommand | 不可靠(丢一两条可接受,下一条会覆盖) | 高(滚动时每秒可达 20-30 次) | < 100ms |
| 购物车状态变更 | 添加商品、删除商品、修改数量、收藏/取消 | ZIM Custom Message | 可靠有序(不能丢) | 低(用户操作的频率) | < 200ms |
| 商品卡片消息 | 标题、缩略图 URL、价格、商品链接 | ZIM Custom Message | 可靠有序 | 低(手动发送) | < 500ms |
4.3 消息通道选型策略
这是架构设计中最需要权衡的决策点。Express SDK 自带三种消息通道,ZIM SDK 另有四种消息类型。如何选择?
Express SDK 自带消息的适用场景:
sendBroadcastMessage(可靠广播,10条/秒,≤ 1024 字节):适合购物车状态初始快照、加入购物车的确认消息。次数少、必须可靠到达。sendCustomCommand(自定义信令,30条/秒,不可靠):适合页面 URL 切换和滚动位置同步。高频、丢几条也没关系。sendBarrageMessage(弹幕消息,不限频,不可靠):适合轻量的文字聊天、表情。
ZIM SDK 消息的适用场景:
ZIMCustomMessage(可靠有序,10条/秒):购物车增删改操作、商品卡片消息、购物决策关键节点的记录。必须保证可靠有序。ZIMCommandMessage(不可靠,30条/秒,≤ 5KB):页面同步指令的高频传输,可承载比 Express 信令更大的 payload。ZIMTextMessage:通话中的普通文字聊天消息。
对于一起购物场景,推荐 Express(媒体 + 自有信令) + ZIM(结构化消息) 的组合:
- 媒体传输:Express SDK(音频、视频、屏幕共享)
- 高频页面同步:ZIM Command Message(30条/秒,5KB,够存 URL + 滚动位置 + 时间戳)
- 购物车操作与卡片消息:ZIM Custom Message(可靠有序,数据一致性有保障)
- 秒级状态同步:Express
sendBroadcastMessage作为备用通道,用于不需要持久化的房间状态
五、核心功能实现
5.1 音视频通话与屏幕共享
这是整个系统的底座。一起购物场景下,屏幕共享是主画面,视频通话是辅助——和会议场景中「人像为主、共享为辅」的布局正好相反。
5.1.1 初始化与登录
// ============ Express SDK 初始化 ============
const zg = new ZegoExpressEngine(appID, server);
// 选择通信场景模式(高音质、低延迟)
zg.setEngineConfig({
scenario: ZegoScenario.HighQualityCommunicate
});
// ============ 创建并登录房间 ============
const token = await fetchTokenFromServer(userID, roomID);
await zg.loginRoom(roomID, token, {
userID: currentUser.id,
userName: currentUser.nickname
});
// ============ 同时初始化 ZIM SDK ============
const zim = ZIM.create({ appID: ZIM_APP_ID });
await zim.login({
userID: currentUser.id,
userName: currentUser.nickname,
token: zimToken
}, token);
// 加入同一个 ZIM 房间(与 RTC 房间分离管理)
await zim.enterRoom({
roomID: shoppingRoomID,
roomName: `一起购物-${roomID}`
});
5.1.2 创建本地音视频流 + 屏幕共享流
一起购物场景中,发起方推两路流:摄像头流 + 屏幕共享流。摄像头流放在小窗,屏幕共享流放在主区域。
// 创建摄像头流(小窗,低分辨率)
const cameraStream = await zg.createZegoStream({
camera: {
video: {
quality: 4, // 自定义模式
width: 240,
height: 320,
frameRate: 15, // 15fps 够用
bitrate: 200 // 低码率,主带宽留给屏幕共享
},
audio: {
bitrate: 48 // 语音码率 48kbps,保证清晰度
}
}
});
// 创建屏幕共享流(主画面,高清晰度)
const screenStream = await zg.createZegoStream({
screen: {
video: {
quality: 2, // 预设值 2:流畅度与清晰度平衡,15fps/1500kbps
},
audio: false // 屏幕共享不需要音频(麦克风已在摄像头流中)
}
});
// 推摄像头流
await zg.startPublishingStream(`camera_${userID}`, cameraStream);
// 推屏幕共享流
await zg.startPublishingStream(`screen_${userID}`, screenStream);
// 监听屏幕共享结束事件(用户点了浏览器的"停止共享"按钮)
zg.on('screenSharingEnded', (stream) => {
console.warn('屏幕共享已结束');
// 通知对方
notifyScreenShareEnded();
});
5.1.3 接收端拉流与布局
接收方同时拉取对方的摄像头流和屏幕共享流,做画中画布局:
// 监听房间流新增事件
zg.on('roomStreamUpdate', async (roomID, updateType, streamList, extendedData) => {
if (updateType === 'ADD') {
for (const streamInfo of streamList) {
// 区分屏幕共享流和摄像头流
const isScreenStream = streamInfo.streamID.startsWith('screen_');
// 拉流
const remoteStream = await zg.startPlayingStream(streamInfo.streamID, {
video: {
// 根据流类型设置不同的渲染 canvas
// 屏幕流渲染到大画布,摄像头流渲染到小窗
renderView: isScreenStream
? document.getElementById('main-screen-canvas')
: document.getElementById('pip-camera-canvas')
}
});
}
}
});
5.1.4 页面 URL 同步
屏幕共享让一方看到另一方的浏览画面,但如果双方想各自在自己的设备上打开同一个商品页(更省带宽、更高清晰度),就需要 URL 同步机制:
// ============ 发起方:监听页面 URL 变化并同步 ============
let lastSentURL = '';
let lastSentScroll = { x: 0, y: 0 };
const SYNC_THROTTLE = 50; // 50ms 节流,避免高频发送
function setupPageSync() {
// 监听 URL 变化(SPA 场景需要 hook history API)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
sendPageSync();
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
sendPageSync();
};
window.addEventListener('popstate', sendPageSync);
// 监听滚动位置(节流)
let scrollTimer = null;
window.addEventListener('scroll', () => {
if (scrollTimer) return;
scrollTimer = setTimeout(() => {
scrollTimer = null;
sendScrollSync();
}, SYNC_THROTTLE);
}, { passive: true });
// 初始发送
sendPageSync();
}
async function sendPageSync() {
const currentURL = window.location.href;
// 避免重复发送相同 URL
if (currentURL === lastSentURL) return;
lastSentURL = currentURL;
// 通过 ZIM Command Message 发送(高频、允许丢失)
const msg = new ZIMCommandMessage({
message: new TextEncoder().encode(JSON.stringify({
type: 'PAGE_URL_SYNC',
url: currentURL,
timestamp: Date.now()
}))
});
await zim.sendMessage(msg, shoppingRoomID, ZIMConversationType.Room);
}
async function sendScrollSync() {
const currentScroll = { x: window.scrollX, y: window.scrollY };
// 滚动位置变化小于 10px 不发送
if (Math.abs(currentScroll.x - lastSentScroll.x) < 10 &&
Math.abs(currentScroll.y - lastSentScroll.y) < 10) return;
lastSentScroll = currentScroll;
const msg = new ZIMCommandMessage({
message: new TextEncoder().encode(JSON.stringify({
type: 'SCROLL_SYNC',
scrollX: currentScroll.x,
scrollY: currentScroll.y,
timestamp: Date.now()
}))
});
await zim.sendMessage(msg, shoppingRoomID, ZIMConversationType.Room);
}
// ============ 接收方:应用同步过来的页面 ============
zim.on('receiveRoomMessage', (messages) => {
messages.forEach(msg => {
if (msg.type !== ZIMMessageType.Command) return;
try {
const data = JSON.parse(new TextDecoder().decode(msg.message));
handleSyncMessage(data);
} catch (e) {
// 解析失败,忽略
}
});
});
function handleSyncMessage(data) {
switch (data.type) {
case 'PAGE_URL_SYNC':
if (window.location.href !== data.url) {
// 在接收端的 iframe 或 WebView 中加载同款商品页
document.getElementById('shared-page-frame').src = data.url;
}
break;
case 'SCROLL_SYNC':
// 同步滚动位置到 iframe
const frame = document.getElementById('shared-page-frame');
if (frame && frame.contentWindow) {
frame.contentWindow.scrollTo(data.scrollX, data.scrollY);
}
break;
}
}
5.2 购物车实时状态同步
购物车同步需要保证可靠有序。使用 ZIM Custom Message 承载每次购物车操作,通过 Server 端维护权威状态来解决并发冲突。
5.2.1 购物车操作的数据结构
// 购物车操作消息格式
const CartActionType = {
ADD: 'ADD', // 添加商品
REMOVE: 'REMOVE', // 移除商品
UPDATE_QTY: 'UPDATE', // 修改数量
TOGGLE_FAVORITE: 'TOGGLE_FAV', // 收藏/取消收藏
CLEAR: 'CLEAR' // 清空购物车
};
function buildCartMessage(actionType, payload) {
return {
type: 'CART_ACTION',
action: actionType,
payload: payload, // { skuId, quantity, price, ... }
userId: currentUser.id,
timestamp: Date.now(),
uuid: crypto.randomUUID() // 幂等键,防止重复处理
};
}
5.2.2 发送购物车操作
async function broadcastCartAction(actionType, payload) {
const messageData = buildCartMessage(actionType, payload);
const customMsg = new ZIMCustomMessage({
message: JSON.stringify(messageData),
subType: 10 // 约定 subType=10 为购物车操作
});
try {
const result = await zim.sendMessage(
customMsg,
shoppingRoomID,
ZIMConversationType.Room,
{ priority: ZIMMessagePriority.High } // 高优先级,保证顺序
);
// 本地已更新,远端将通过 receiveRoomMessage 同步
console.log('Cart action sent:', result);
} catch (err) {
console.error('Cart sync failed:', err);
// 重试逻辑或回滚本地状态
}
}
5.2.3 接收并应用购物车操作
zim.on('receiveRoomMessage', (messages) => {
messages.forEach(msg => {
if (msg.type !== ZIMMessageType.Custom) return;
if (msg.subType !== 10) return; // 不是购物车操作
try {
const cartAction = JSON.parse(msg.message);
applyCartAction(cartAction);
} catch (e) {
console.error('Invalid cart message:', e);
}
});
});
function applyCartAction(action) {
// 幂等检查:如果已经处理过(通过 uuid),跳过
if (processedActionUUIDs.has(action.uuid)) return;
processedActionUUIDs.add(action.uuid);
// 忽略自己发出的操作(避免重复应用)
if (action.userId === currentUser.id) return;
switch (action.action) {
case CartActionType.ADD:
localCart.addItem(action.payload);
break;
case CartActionType.REMOVE:
localCart.removeItem(action.payload.skuId);
break;
case CartActionType.UPDATE_QTY:
localCart.updateQuantity(action.payload.skuId, action.payload.quantity);
break;
case CartActionType.TOGGLE_FAVORITE:
localCart.toggleFavorite(action.payload.skuId);
break;
case CartActionType.CLEAR:
localCart.clear();
break;
}
// 触发 UI 刷新
renderCartUI();
}
5.2.4 新用户加入时的购物车全量同步
当新用户加入房间时,需要获取当前的购物车全量快照:
// 房间属性中维护当前购物车的摘要信息(用于发现快照版本)
const cartSummary = {
version: cartVersion, // 递增版本号
itemCount: 5,
lastModified: Date.now()
};
// 当有人加入房间
zim.on('roomMemberJoined', async (data) => {
// 当前用户是房间内的「老成员」,推送购物车快照给新成员
const snapshotMsg = new ZIMCustomMessage({
message: JSON.stringify({
type: 'CART_SNAPSHOT',
version: cartVersion,
items: localCart.toJSON() // 完整的购物车数据
}),
subType: 11 // 约定 subType=11 为购物车快照
});
// 发送给新加入的成员
await zim.sendMessage(snapshotMsg, shoppingRoomID, ZIMConversationType.Room);
});
// 新成员端:接收到快照后用其初始化本地购物车
// 在 receiveRoomMessage 中处理 subType=11 的消息
function handleCartSnapshot(snapshot) {
localCart.loadFromSnapshot(snapshot.items);
cartVersion = snapshot.version;
// 同步后,此前的增量操作基于快照版本进行
renderCartUI();
}
5.3 自定义商品卡片消息
商品卡片是一种结构化消息,在聊天/通话界面以卡片形式展示商品信息。它比发一串链接文字更直观,点击可直接跳转商品页。
// ============ 商品卡片消息定义 ============
function buildProductCard(product) {
return {
type: 'PRODUCT_CARD',
subType: 20, // 约定 subType=20 为商品卡片
data: {
title: product.title,
imageURL: product.thumbnail,
price: product.price,
originalPrice: product.originalPrice,
platform: product.platform, // 淘宝/京东/拼多多等
productURL: product.url,
rating: product.rating,
sales: product.sales,
shopName: product.shopName
}
};
}
// ============ 发送商品卡片 ============
async function sendProductCard(productInfo) {
const card = buildProductCard(productInfo);
const customMsg = new ZIMCustomMessage({
message: JSON.stringify(card),
subType: 20,
searchedContent: productInfo.title // 可搜索字段,方便后续查找
});
await zim.sendMessage(customMsg, shoppingRoomID, ZIMConversationType.Room);
}
// ============ 渲染商品卡片(接收端) ============
zim.on('receiveRoomMessage', (messages) => {
messages.forEach(msg => {
if (msg.type !== ZIMMessageType.Custom) return;
if (msg.subType !== 20) return; // 不是商品卡片
const card = JSON.parse(msg.message);
renderProductCard(card.data, msg.senderUserID);
});
});
function renderProductCard(product, senderId) {
const cardHTML = `
<div class="product-card" onclick="navigateToProduct('${product.productURL}')">
<img src="${product.imageURL}" alt="${product.title}" class="card-thumb" />
<div class="card-info">
<div class="card-title">${product.title}</div>
<div class="card-price">
<span class="current-price">¥${product.price}</span>
${product.originalPrice ? `<span class="original-price">¥${product.originalPrice}</span>` : ''}
</div>
<div class="card-meta">
<span>${product.platform}</span>
<span>${product.shopName}</span>
</div>
</div>
</div>
`;
// 插入到聊天/通话面板中
appendToMessagePanel(cardHTML, senderId);
}
商品卡片消息采用 ZIM CustomMessage 承载,subType=20,可靠有序保证卡片不丢。searchedContent 字段填入商品标题,后续可以通过 ZIM 的消息搜索能力按商品名查找历史讨论过的商品。
5.4 云端录制购物决策过程
记录「逛→选→讨论→决策」的完整链路,既是一起购物的纪念,也可以作为社交分享素材或平台内容沉淀。
// ============ 业务服务端启动云端录制(通过 ZEGO 服务端 API) ============
// 注意:云端录制通过服务端 API 启动,非客户端 SDK
// POST https://zego-cloud-recording-api/start
const startRecordingRequest = {
RoomId: shoppingRoomID,
RecordInputParams: {
RecordMode: 2, // 混流录制模式
StreamType: 3, // 音视频都录
MixConfig: {
MixMode: 3, // 自定义混流布局
MixOutputVideoConfig: {
Width: 1280,
Height: 720,
Fps: 15,
Bitrate: 1500000
}
},
MaxIdleTime: 60 // 房间无流 60 秒后自动停止录制
},
RecordOutputParams: {
OutputFileFormat: 'mp4',
OutputFolder: 'co-shopping/' + shoppingRoomID + '/'
},
StorageParams: {
Vendor: 3, // 腾讯云 COS
Region: 'ap-guangzhou',
Bucket: 'your-bucket',
AccessKeyId: 'xxx',
AccessKeySecret: 'xxx'
}
};
// ============ 客户端触发录制(通过业务服务器中转) ============
async function startCloudRecording() {
const response = await fetch('/api/recording/start', {
method: 'POST',
body: JSON.stringify({ roomID: shoppingRoomID })
});
return response.json();
}
async function stopCloudRecording() {
const response = await fetch('/api/recording/stop', {
method: 'POST',
body: JSON.stringify({ roomID: shoppingRoomID })
});
return response.json();
}
// 录制回调通知(服务端接收)
// ZEGO 云端录制完成后会回调业务服务器,通知文件上传完成
// 业务服务器再将录制信息通过 ZIM 消息告知房间成员
云端录制的混流布局建议:屏幕共享流为主画面(占 70% 面积),摄像头小窗在右下角,购物车操作的时间轴在左侧以字幕形式呈现。这样回看时能清晰还原在哪个页面讨论了什么、最终加了什么商品。
5.5 Token 鉴权
// ============ 服务端(Node.js)生成 Token ============
const ZegoServerAssistant = require('zego-server-assistant');
function generateExpressToken(appID, serverSecret, userID, roomID) {
const payload = {
room_id: roomID,
privilege: {
1: 1, // loginRoom 权限
2: 1 // publishStream 权限
},
stream_id_list: []
};
return ZegoServerAssistant.generateToken04(
appID * 1,
userID,
serverSecret,
3600 * 4, // 4小时有效期
JSON.stringify(payload)
);
}
// ZIM Token 生成也类似,两个 SDK 使用各自的 AppID 和 Secret
function generateZIMToken(appID, serverSecret, userID) {
return ZegoServerAssistant.generateToken04(
appID * 1,
userID,
serverSecret,
3600 * 4,
'' // ZIM Token 无需额外 payload
);
}
六、关键问题与优化策略
| 序号 | 问题 | 场景影响 | 优化策略 |
|---|---|---|---|
| 1 | 屏幕共享的隐私泄露 | 用户分享屏幕时意外暴露微信消息弹窗、支付密码输入框、银行App通知等敏感信息 | 三层防护:① 浏览器层面,使用标签页共享而非整个屏幕共享(getDisplayMedia 仅选择浏览器标签页而非整个桌面);② 应用层面,检测到用户切换到支付页面时自动暂停屏幕共享流(zg.stopPublishingStream),弹出「已暂停共享」提示;③ UI 层面,屏幕共享区域上方常驻半透明遮罩提示「正在共享屏幕」,让用户保持警觉 |
| 2 | 不同设备分辨率下的浏览体验不一致 | 一方用 iPhone 15 Pro(393×852 viewport),另一方用 iPad(1024×1366),同一商品页在不同设备上布局迥异,讨论时「左上角那个按钮」指的未必是同一个位置 | 屏幕共享为主、URL 同步为辅:双方默认通过屏幕共享看到完全相同的画面(等同于一方设备上的视觉镜像)。同时下发 URL 让接收方在自己设备上打开,作为可选视图。滚动位置同步时,基于百分比而非绝对像素(scrollPercent = scrollY / (docHeight - viewHeight)),让不同屏幕比例下有相同的相对定位 |
| 3 | 弱网下语音 + 屏幕共享的资源竞争 | 4G 弱信号下(比如商场地下层),带宽不足以同时维持清晰语音和高清屏幕共享,出现音频卡顿或屏幕共享画面严重模糊 | 动态降级策略:① 监听 publishQualityUpdate 回调获取实时网络质量 score;② 当网络评分 < 2(中差)时,自动降低屏幕共享编码配置——帧率从 15fps 降到 5fps,码率从 1500kbps 降到 600kbps,保证语音通道不受影响;③ 当网络评分 < 1(极差)时,屏幕共享切换为 URL 同步模式(文本指令极低带宽),只保留语音通道;④ Express SDK 的 FEC(前向纠错)和自适应码率在此过程中自动工作,开发者只需关注降级策略的触发条件 |
| 4 | 购物车并发写入冲突 | 双方同时修改同一商品的数量(比如 A 把数量改成 2,B 同时改成 3),各自本地购物车状态不一致 | Last-Write-Wins + 服务端仲裁:① 每条购物车操作消息携带 timestamp(毫秒级 UTC 时间戳)和 userId;② 两个操作到达时,通过 timestamp 比较——以最后一次操作为准;③ 如果时间戳完全相同(极小概率),以 userId 字典序更靠后的一方为准;④ 服务端周期性拉取房间内购物车状态做持久化,客户端登录时从服务端拉取最新版本,避免本地与云端长期不一致 |
| 5 | 屏幕共享的音频处理 | 用户共享标签页播放商品视频时,对方既听到共享的媒体声音又听到麦克风声音,产生回声或双重音频 | 屏幕共享流不采集音频:createZegoStream 的 screen.audio 设为 false,只共享画面不共享音频。如果确实需要共享媒体声音(如一起看商品介绍视频),使用「URL 同步 + 各自本地播放」的方案——双方同时打开同一个视频页面,通过 Command Message 同步播放进度(当前时间戳、暂停/播放状态),保证音画同步的同时避免回声 |
| 6 | 通话中切换购物平台 | 一起购物流程中可能跨平台跳转——从小红书种草链接切到淘宝比价,再切到京东看评论。屏幕共享的设置需要频繁变更 | 保持屏幕共享流的稳定性:不要每次切换标签页就重建流。推荐共享整个浏览器窗口(而非单个标签页),这样标签页切换不影响共享流的连续性。同时,通过 sendCustomCommand 同步当前浏览的 URL,让接收方知道对方在看哪个平台,本地可以预加载或辅助搜索 |
七、场景延伸与扩展玩法
7.1 AI 购物助手实时推荐
技术思路:利用 Express SDK 的音频原始数据回调获取通话音频 → 云端实时语音识别(ASR)转写为文本 → NLP 提取商品特征关键词(显白、通勤、平替等语义标签)→ 向大模型或电商搜索 API 查询相似/对比商品 → 结果通过 ZIM CustomMessage(商品卡片格式)推送到通话界面。
关键组件:
– Express SDK 的 onAudioRawData 回调获取 PCM 音频数据
– ZEGO 云端实时语音识别服务进行流式转写
– 业务服务端做 NLP 意图识别和推荐排序
– 推荐结果以商品卡片消息实时推送,不打断通话
7.2 AR 虚拟试穿 + 视频通话
技术思路:在视频通话中叠加 AR 特效,一方试穿虚拟服装/配饰,另一方通过视频画面实时看到效果并给出意见。
实现路径:
– 基于 Express SDK 的自定义视频采集或视频前处理,在摄像头画面送入编码器前插入 AR 渲染管线
– AR 引擎(如 Web 端的 Three.js + MediaPipe 骨骼检测)在 CV 帧上叠加虚拟商品(墨镜、口红、服装轮廓)
– 处理后的帧交给 Express SDK 推流,对方看到的是带有 AR 叠加的画面
– 虚拟商品信息通过 ZIM CustomMessage 同步(试穿了哪件、颜色、尺码),让对方端也能展示对应的商品详情卡片
7.3 达人陪逛(C2C 导购服务)
技术思路:将 1v1 的闺蜜逛街扩展为「达人 + 消费者」的 C2C 导购模式。达人(穿搭博主、美妆 KOL)与消费者建立实时音视频连接,一边屏幕共享浏览商品,一边提供搭配建议和选购指导。
技术要点:
– 多人通话模式(1 达人 + 1-3 位消费者),Express SDK 支持多路音视频
– 达人端屏幕共享为主流,消费者端只传摄像头(默认关闭,可选开启)
– 达人可同时向多位消费者展示不同商品,通过 ZIM 的房间属性设置不同消费者的当前商品视图
– 通话结束后,达人的推荐清单通过 ZIM CustomMessage 自动存档,消费者可在聊天历史中回溯所有被推荐商品
– 计费模式可通过通话时长 + 商品成交抽佣实现,借助云端录制做交易凭证
7.4 社交购物直播
技术思路:将一对一的共览升级为一对多的购物直播。主播发起购物直播,观众可以举手连麦参与讨论,弹幕刷商品链接,直播中直接完成购买。
技术要点:
– Express SDK 的旁路推流能力:主播的屏幕共享流 + 摄像头流 → 混流 → 转推 CDN,支撑上千观众同时观看
– CDN 播放端通过 RTMP/HLS 拉流,延迟 1-3 秒(可接受)
– 互动功能走 ZIM 房间消息:弹幕、点赞、商品卡片推送
– 需要连麦讨论的观众,从 CDN 切回 RTC 低延迟模式,上麦后走 Express 音频通道
– 互动直播 UIKit 可提供开播/看播的预制 UI,包括商品橱窗、购物袋、讲解中商品高亮等组件
– 购物车数据通过 ZIM 服务端 API 直接写入用户账户,不依赖客户端同步
7.5 一起逛的异步延续
技术思路:通话结束不是一起购物的终点。本次逛过的商品、讨论过的决策点、最终下单的商品,都应该沉淀为一份「购物回顾 Timeline」,让用户异步回溯。
技术要点:
– 云端录制生成的回放视频作为「购物 vlog」
– 通话期间通过 ZIM CustomMessage 发送的所有商品卡片消息按时间轴排列,生成文字版购物路线
– 未下单但讨论热烈的商品自动加入「待定清单」,下次通话时可从上次的购物车继续
– ZIM 的消息搜索能力支持按商品名检索历史通话中讨论过的商品——「上次一起看的那条裙子叫什么来着?」
八、总结
一起购物是实时音视频技术与电商场景交叉的前沿方向,其技术实现难度不亚于一个完整的视频会议系统。核心要点:
- 屏幕共享是体验基石,但隐私控制是上线前提。标签页级共享 + 应用层悬停提示 + 支付页面自动暂停的三层防护,是避免隐私事故的最低配置。在屏幕共享设计上,始终假设用户会忘记自己在共享屏幕。
- 消息通道不能一刀切。页面同步指令(高频、可丢)、购物车操作(低频、必须可靠有序)、商品卡片(结构化、可搜索)是三种截然不同的通信需求。ZIM Command Message + ZIM Custom Message + Express 自带信令的组合,比的不是在单一维度上做到极致,而是每种数据找到最匹配的通道。
- 购物车同步的本质是分布式状态机。Last-Write-Wins 策略在大部分场景够用,但需要
uuid做幂等、timestamp做排序、服务端做兜底持久化。当一方网络中断恢复后,服务端的全量快照是最可靠的修复方式。 - 架构上,信令与媒体分离是基本纪律。音视频和屏幕共享走 Express RTC 网络(UDP/FEC/自适应),结构化消息走 ZIM 通道(可靠投递 + 有序性 + 离线存储),业务逻辑在自建服务端处理。三条线各自为战但目标统一,不给任何一条线加不该它承担的负载。
技术选型方面,ZEGO Express SDK + ZIM SDK 的组合覆盖了一起购物场景从媒体传输到消息同步、从屏幕共享到云端录制的全部基础设施需求。开发团队的精力应该放在购物车状态机设计、屏幕共享隐私策略、商品卡片交互和跨平台购物体验打磨上——这才是一起购物产品真正拉开体验差距的地方。
本文技术方案基于 ZEGO Express SDK 和 ZIM 即时通讯 SDK 撰写,具体 API 细节和版本更新建议参考 ZEGO 官方文档。
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/info/67363.html