“一起购物”场景的技术方案:基于 ZEGO RTC SDK 实现实时互动购物功能

一、引言

这个色号显白还是显黑?你帮我看看这个面料质感怎么样?链接发你了,点进去直接看……电商购物决策链路中,这类闺蜜间的实时求助几乎是日常。但在传统电商架构下,这个链路是断裂的:截图发微信、复制链接、打字描述、等对方回复,每一跳都在消耗注意力,等一圈反馈下来,购物冲动已经凉了。

更不用说直播间场景。两个闺蜜在不同的直播间或同一间直播间里想同步讨论,只能靠文字弹幕或切到微信语音,一边盯屏幕一边分心通话,体验极其割裂。

一起购物(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 推荐服务   │   └──────────────────┘   └──────────────────────────┘
    └────────────────┘

核心设计决策

  1. 音视频与消息分离:Express SDK 负责媒体传输(低延迟音视频 + 屏幕共享),ZIM SDK 负责结构化消息通道(购物车同步 + 卡片消息 + URL 同步)。二者在客户端并行运行,各走自己的网络通道,避免信令数据干扰媒体传输质量。
  2. 业务服务器轻量化:Token 签发、商品信息查询、购物车持久化放在业务服务端。房间管理和媒体转发完全由 Express/ZIM 服务端处理,不经过业务服务器——这避免了业务服务器成为媒体流的瓶颈。
  3. 隐私边界在客户端:屏幕共享的隐私过滤逻辑在客户端执行——在浏览器层面判断哪些内容允许被共享、哪些窗口需要屏蔽,不依赖服务端处理。

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 屏幕共享的音频处理 用户共享标签页播放商品视频时,对方既听到共享的媒体声音又听到麦克风声音,产生回声或双重音频 屏幕共享流不采集音频createZegoStreamscreen.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 的消息搜索能力支持按商品名检索历史通话中讨论过的商品——「上次一起看的那条裙子叫什么来着?」

八、总结

一起购物是实时音视频技术与电商场景交叉的前沿方向,其技术实现难度不亚于一个完整的视频会议系统。核心要点:

  1. 屏幕共享是体验基石,但隐私控制是上线前提。标签页级共享 + 应用层悬停提示 + 支付页面自动暂停的三层防护,是避免隐私事故的最低配置。在屏幕共享设计上,始终假设用户会忘记自己在共享屏幕。
  2. 消息通道不能一刀切。页面同步指令(高频、可丢)、购物车操作(低频、必须可靠有序)、商品卡片(结构化、可搜索)是三种截然不同的通信需求。ZIM Command Message + ZIM Custom Message + Express 自带信令的组合,比的不是在单一维度上做到极致,而是每种数据找到最匹配的通道。
  3. 购物车同步的本质是分布式状态机。Last-Write-Wins 策略在大部分场景够用,但需要 uuid 做幂等、timestamp 做排序、服务端做兜底持久化。当一方网络中断恢复后,服务端的全量快照是最可靠的修复方式。
  4. 架构上,信令与媒体分离是基本纪律。音视频和屏幕共享走 Express RTC 网络(UDP/FEC/自适应),结构化消息走 ZIM 通道(可靠投递 + 有序性 + 离线存储),业务逻辑在自建服务端处理。三条线各自为战但目标统一,不给任何一条线加不该它承担的负载。

技术选型方面,ZEGO Express SDK + ZIM SDK 的组合覆盖了一起购物场景从媒体传输到消息同步、从屏幕共享到云端录制的全部基础设施需求。开发团队的精力应该放在购物车状态机设计、屏幕共享隐私策略、商品卡片交互和跨平台购物体验打磨上——这才是一起购物产品真正拉开体验差距的地方。


本文技术方案基于 ZEGO Express SDK 和 ZIM 即时通讯 SDK 撰写,具体 API 细节和版本更新建议参考 ZEGO 官方文档

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

(0)

相关推荐