带看集成指南
本文档面向接入众趣 zqsdk 的开发者,说明如何在自己的项目中实现「带看」功能。带看即两个用户(发起方与接收方)通过 IM 实时同步 VR 视角与操作,实现远程协同看房体验。你的项目需要:通过 URL 参数区分角色、集成 IM 通道、注册 SDK 带看事件、处理消息协议,并实现带看入口、场景列表与操作权限蒙层等 UI。
一、功能简介与前置条件
1.1 什么是带看
带看是基于 zqsdk 的实时协同看房能力:两个用户各自打开同一模型的 VR 页面,通过 IM(即时通讯)通道实时同步相机视角、场景切换、标尺操作等,实现「一方操作,另一方同步跟随」的远程带看体验。
核心能力包括:
- 视角实时同步:一方移动/旋转相机,另一方自动跟随到相同视角。
- 场景切换同步:一方切换到某个场景(如客厅、主卧),另一方同步跳转。
- 标尺操作同步:一方开启/关闭全景标尺,另一方同步显隐。
- 操作权限管理:SDK 自动管理操作权限(本地/远端/自由),当远端正在操作时,本地显示「对方正在操作」蒙层。
1.2 角色说明
带看涉及两个角色,通过 URL 参数区分:
| 角色 | URL 参数 | 启动方式 | 说明 |
|---|---|---|---|
| 发起方 | showtakelook=on |
正常启动 housePlay.start() |
页面上显示「带看」按钮,用户点击后建立 IM 连接 |
| 接收方 | takelook=on |
引导模式 housePlay.start('guid') |
页面加载后自动初始化 IM,等待发起方同步视角 |
1.3 前置条件
- 已接入众趣 zqsdk,并在
zqsdk.init的complete回调中拿到window.housePlay(参见 VR 初始化展示指南)。 - 已准备好 IM 服务:本仓库默认使用网易云信 NIM SDK,你也可以替换为腾讯云 TIM 或其他 IM 服务(需实现适配器接口)。
- IM 相关凭证(
appid、userid、sig、funcuserid)由你的业务后端生成,通过 URL 参数传入前端页面。
1.4 示例截图

二、整体架构与消息协议
2.1 架构概览
┌─────────────────────────────────────────────────────────┐
│ 发起方页面 │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ 带看按钮 │──▶│ TakeLook │──▶│ IM 适配器 (NIM) │───┐│
│ └──────────┘ │ Store │ └───────────────────┘ ││
│ │ │◀── housePlay 带看事件 ││
│ └──────────┘ ││
└─────────────────────────────────────────────────────────┘│
│
IM 通道(网易云信 / 腾讯云等) │
│
┌─────────────────────────────────────────────────────────┐│
│ 接收方页面 ││
│ ┌──────────────┐ ┌──────────┐ ┌─────────────────┐ ││
│ │ 操作权限蒙层 │◀──│ TakeLook │◀──│ IM 适配器 (NIM) │◀─┘│
│ │ 场景列表面板 │ │ Store │ └─────────────────┘ │
│ └──────────────┘ │ │──▶ housePlay 同步方法 │
│ └──────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 消息协议
带看通过 IM 传输的消息遵循统一的 TakeLookMessage 协议,分为状态类和操作类两种:
状态类消息(连接初始化)
用于双方建立连接并完成初始视角同步:
interface TakeLookStateMessage {
type: 'state'
data: {
type: 'connectState'
state: 'initdone' | 'initstate' | 'initstatedone'
data?: unknown // initstate 时携带当前 3D 坐标与角度
}
}
操作类消息 — 核心操作(相机同步)
用于实时同步相机移动、旋转等操作:
interface TakeLookCoreOperation {
type: 'operation'
data: {
type: 'CORE_OPERATION'
data: unknown // SDK uploadLocalActions 事件的原始数据
}
}
操作类消息 — UI 操作(场景切换、标尺等)
用于同步场景切换、标尺显隐等 UI 级操作:
interface TakeLookUIOperation {
type: 'operation'
data: {
type: 'UI_OPERATION'
data: {
type: 'UI'
target: 'switchScene' | 'ruler' | string
action: unknown // switchScene 时为场景对象,ruler 时为 boolean
}
}
}
2.3 连接初始化握手流程
双方通过三次状态消息完成初始视角同步:
发起方 接收方
│ │
│ SDK 资源加载完成 │ SDK 资源加载完成
│ (LocalProgramLoadingCompleted) │ (LocalProgramLoadingCompleted)
│ │
│ IM 连接成功后补发 │
│──── initdone ──────────────────────────▶│
│ │ 收到 initdone
│ │ 获取当前 3D 坐标
│◀──── initstate (携带坐标数据) ───────────│
│ │
│ 收到 initstate │
│ 执行 firstSyncRemoteActions │
│ (同步到对方视角) │
│ │
│ firstSyncActionsCompleted 触发 │
│ setConnectionStatus(true) │
│──── initstatedone ─────────────────────▶│
│ │ 收到 initstatedone
│ │ setConnectionStatus(true)
│ │
│◀═══════ 实时视角同步开始 ═══════════════▶│
三、集成步骤概览
| 步骤 | 内容 |
|---|---|
| 1 | 准备 URL 参数,区分发起方(showtakelook=on)与接收方(takelook=on) |
| 2 | 集成 IM 通道(NIM / TIM 或自定义适配器) |
| 3 | 在 SDK complete 回调中判断带看模式,注册带看事件并按角色启动 |
| 4 | 实现消息收发与协议处理逻辑 |
| 5 | 实现带看 UI:入口按钮、场景列表面板、操作权限蒙层 |
| 6 | (可选)实现 iframe 嵌入的双端演示页面 |
以下按步骤展开。
四、第一步:准备 URL 参数
带看功能通过 URL 参数传递角色信息和 IM 凭证。你的业务后端需要为双方各生成一组参数。
4.1 参数列表
| 参数 | 必填 | 说明 |
|---|---|---|
m |
是 | 模型 ID |
showtakelook |
发起方必填 | 设为 on 表示显示带看入口按钮 |
takelook |
接收方必填 | 设为 on 表示以带看接收方模式启动 |
appid |
是 | IM 服务的 AppKey / AppID |
userid |
是 | 当前用户的 IM 账号 |
sig |
是 | 当前用户的 IM 鉴权签名 / Token |
funcuserid |
是 | 对方用户的 IM 账号(消息发送目标) |
4.2 URL 示例
发起方(经纪人端):
https://your-domain.com/vr?m=模型ID&showtakelook=on&appid=YOUR_APP_ID&userid=agent001&sig=TOKEN_A&funcuserid=customer001
接收方(客户端):
https://your-domain.com/vr?m=模型ID&takelook=on&appid=YOUR_APP_ID&userid=customer001&sig=TOKEN_B&funcuserid=agent001
注意 userid 和 funcuserid 是互换的:发起方的 funcuserid 是接收方的 userid,反之亦然。
五、第二步:集成 IM 通道
带看需要一个 IM 通道来传输消息。本仓库提供了适配器模式,默认实现了网易云信 NIM 适配器,并预留了腾讯云 TIM 适配器接口。
5.1 IM 适配器接口
无论使用哪种 IM 服务,都需要实现以下接口:
interface IIMAdapter {
/** 初始化并连接 */
init(options: { appId: string; userId: string; userSig: string }): Promise<void>
/** 发送消息给指定用户 */
sendMessage(toUserId: string, data: TakeLookMessage): void
/** 断开连接并清理 */
destroy?(): void
/** 监听事件(至少支持 'message' 事件) */
on?(event: string, callback: (data: unknown) => void): void
}
收到消息时,适配器需要解析 IM 原始消息并 emit 一个 message 事件,数据格式为:
interface IMReceivedMessage {
from: string // 发送方 userId
to?: string // 接收方 userId
content: TakeLookMessage // 解析后的带看业务消息
}
5.2 使用网易云信 NIM(默认)
安装依赖:
npm install netease-nim-web-sdk
NIM 适配器的核心逻辑:
- 初始化:调用
NIM.getInstance传入appKey(即 appId)、account(即 userId)、token(即 sig),在onconnect回调中 resolve。 - 接收消息:在
oncustomsysmsg回调中解析消息内容。NIM 的自定义系统消息content为 JSON 字符串,格式为{ type: 'VRContent', VRContent: '...' },需要两次 JSON.parse 提取出TakeLookMessage。 - 发送消息:调用
imInstance.sendCustomSysMsg,将TakeLookMessage序列化后包装为{ type: 'VRContent', VRContent: JSON.stringify(data) }发送。
// 接收消息的解析逻辑
function parseNIMMessage(msg) {
let content = JSON.parse(msg.content) // { type: 'VRContent', VRContent: '...' }
let business
if (content.type === 'VRContent' && content.VRContent != null) {
business = JSON.parse(content.VRContent) // TakeLookMessage
} else {
business = content
}
return { from: msg.from, content: business }
}
// 发送消息的封装逻辑
function sendNIMMessage(imInstance, toUserId, data) {
let msg = { VRContent: JSON.stringify(data), type: 'VRContent' }
imInstance.sendCustomSysMsg({
scene: 'p2p',
to: toUserId,
isPushable: false,
needPushNick: false,
content: JSON.stringify(msg),
sendToOnlineUsersOnly: false,
apnsText: JSON.stringify(msg),
done: function(error) {
if (error) console.warn('NIM send failed', error)
}
})
}
5.3 替换为其他 IM 服务
若需使用腾讯云 TIM 或其他 IM,只需实现 IIMAdapter 接口并替换适配器创建函数即可。适配器内部负责:
- 连接 IM 服务
- 将收到的原始消息解析为
IMReceivedMessage格式并 emitmessage事件 - 将
TakeLookMessage序列化后通过 IM 发送
六、第三步:在 SDK complete 中判断带看模式并启动
在 zqsdk.init 的 complete 回调中,根据 URL 参数判断是否为带看模式,并按角色执行不同的启动逻辑。
complete: function() {
let housePlay = window.housePlay
if (!housePlay) return
// 先注册基础事件(加载进度、UI 就绪等)
registerBaseEvents(housePlay)
let isTakeLookParam = getUrlParam('takelook') === 'on' // 接收方
let isShowTakeLookParam = getUrlParam('showtakelook') === 'on' // 发起方
let isTakeLookMode = isTakeLookParam || isShowTakeLookParam
if (isTakeLookMode) {
// 注册带看事件(见第四步)
initTakeLookEvents(housePlay)
if (isTakeLookParam) {
// ── 接收方 ──
// 以 guid 模式启动,等待发起方同步视角
housePlay.start('guid')
// 自动初始化 IM
let appid = getUrlParam('appid')
let userid = getUrlParam('userid')
let sig = getUrlParam('sig')
let funcuserid = getUrlParam('funcuserid')
if (appid && userid && sig && funcuserid) {
initIM({ appid, userid, sig, funcuserid })
}
} else {
// ── 发起方 ──
// 正常启动,用户点击「带看」按钮后再建立 IM
housePlay.start()
}
} else {
// 非带看模式:正常启动
housePlay.start()
}
}
关键区别:
- 接收方使用 housePlay.start('guid') 以引导模式启动,SDK 不会自动开始漫游,而是等待远端同步视角。
- 发起方使用 housePlay.start() 正常启动,用户可自由操作,点击带看按钮后才建立 IM 连接。
七、第四步:注册带看事件与消息处理
7.1 注册 housePlay 带看事件
在 housePlay 可用后注册以下带看专用事件:
function initTakeLookEvents(housePlay) {
// 1. SDK 资源加载完成 → 发送 initdone 开始握手
housePlay.on('LocalProgramLoadingCompleted', function() {
sendMessage({
type: 'state',
data: { state: 'initdone', type: 'connectState' }
})
})
// 2. 本地操作上传 → 发送 CORE_OPERATION 同步相机
housePlay.on('uploadLocalActions', function(data) {
sendMessage({
type: 'operation',
data: { type: 'CORE_OPERATION', data: data }
})
})
// 3. 首次同步完成 → 设置连接状态并通知对方
housePlay.on('firstSyncActionsCompleted', function() {
housePlay.setConnectionStatus(true)
sendMessage({
type: 'state',
data: { state: 'initstatedone', type: 'connectState' }
})
})
// 4. 操作权限变化 → 控制「对方正在操作」蒙层
housePlay.on('validOperatorChanged', function(operator) {
if (operator === 'remote') {
yourApp.showRemoteOperatingOverlay()
} else {
yourApp.hideRemoteOperatingOverlay()
}
})
}
7.2 处理收到的消息
IM 适配器收到消息后,按消息类型分发处理:
function handleReceiveMessage(msg) {
let housePlay = window.housePlay
if (!housePlay) return
let content = msg.content
// ── 操作类消息 ──
if (content.type === 'operation') {
// 设置操作权限为远程正在操作
housePlay.setValidOperator('remote')
if (content.data.type === 'CORE_OPERATION') {
// 核心操作:同步相机视角
housePlay.syncRemoteActions(content.data.data)
} else if (content.data.type === 'UI_OPERATION') {
let target = content.data.data.target
let action = content.data.data.action
if (target === 'switchScene') {
// 场景切换
housePlay.warpToScene(action)
} else if (target === 'ruler') {
// 标尺显隐
yourApp.setRulerVisible(!!action)
}
}
return
}
// ── 状态类消息(连接初始化握手) ──
if (content.type === 'state' && content.data.type === 'connectState') {
let state = content.data.state
if (state === 'initdone') {
// 对方资源加载完成,回复当前视角状态
let currentState = housePlay.getCurrentState()
sendMessage({
type: 'state',
data: { type: 'connectState', state: 'initstate', data: currentState }
})
} else if (state === 'initstate' && content.data.data != null) {
// 收到对方视角,执行首次同步
housePlay.firstSyncRemoteActions(content.data.data)
} else if (state === 'initstatedone') {
// 握手完成,开始实时同步
housePlay.setConnectionStatus(true)
}
}
}
7.3 补发 initdone 的时机
发起方通常在用户点击「带看」按钮后才建立 IM 连接,此时 SDK 的 LocalProgramLoadingCompleted 事件可能已经触发过了。因此 IM 连接成功后需要补发 initdone,否则双方无法完成握手:
function initIM(options) {
// ... IM 初始化 ...
adapter.init({ appId, userId, userSig }).then(function() {
// IM 连接成功
// 如果 SDK 资源已加载完成,补发 initdone
if (localProgramLoaded) {
sendMessage({
type: 'state',
data: { state: 'initdone', type: 'connectState' }
})
}
})
}
7.4 场景切换与标尺同步
在带看模式下,场景切换和标尺操作需要同时执行本地操作并发送 IM 消息给对方:
// 场景切换:本地跳转 + 发送消息
function switchScene(scene) {
sendMessage({
type: 'operation',
data: {
type: 'UI_OPERATION',
data: { type: 'UI', target: 'switchScene', action: scene }
}
})
housePlay.gotoScenebyId(scene.index)
}
// 标尺同步:仅在带看已连接时发送
function rulerOperation(open) {
if (!isTakeLookConnected) return
sendMessage({
type: 'operation',
data: {
type: 'UI_OPERATION',
data: { type: 'UI', target: 'ruler', action: open }
}
})
}
八、第五步:实现带看 UI
UI 完全由你自定义,建议具备以下三个组件:
8.1 带看入口按钮
显示条件:SDK 已就绪 + URL 含 showtakelook=on + 尚未建立带看连接 + 非讲房模式 + 非换装模式。
点击行为:
1. 从 URL 参数中获取 appid、userid、sig、funcuserid
2. 调用 IM 初始化建立连接
3. (可选)通过 postMessage 通知父页面,由父页面为接收方生成 URL 并打开
function handleTakeLookBtnClick() {
let appid = getUrlParam('appid')
let userid = getUrlParam('userid')
let sig = getUrlParam('sig')
let funcuserid = getUrlParam('funcuserid')
initIM({ appid, userid, sig, funcuserid }).then(function() {
// 可选:通知父页面(iframe 嵌入场景)
window.top.postMessage({
msg: 'MessageFromIframePage',
data: {
appid: appid,
userid: funcuserid, // 对方的 userid
sig: sig,
funcuserid: userid // 当前用户作为对方的 funcuserid
}
}, '*')
})
}
8.2 场景列表面板
显示条件:带看已连接 + 场景列表不为空。
数据来源:从 housePlay.model 中提取场景列表。优先读取 settings.nestscenes.scenes,若不存在则回退到 model.heroLocations。过滤掉「总述」和脚本项。
function getScenesFromHousePlay(housePlay) {
// 优先使用 nestscenes
let scenes = housePlay.settings?.nestscenes?.scenes
if (scenes && scenes.length > 0) {
return scenes
.filter(function(s) { return s.name && s.name !== '总述' && s.script !== 1 })
.map(function(s, i) { return { name: s.name, index: i, id: s.id } })
}
// 回退到 heroLocations
let heroLocations = housePlay.model?.heroLocations
if (heroLocations && heroLocations.length > 0) {
return heroLocations
.filter(function(s) { return s?.name && s.name !== '总述' && s.script !== 1 })
.map(function(s, i) { return { name: s.name, index: i } })
}
return []
}
交互:点击场景卡片时调用 switchScene(scene) 同时执行本地跳转和远端同步。
8.3 操作权限蒙层
显示条件:当 SDK 触发 validOperatorChanged 且 operator 为 'remote' 时显示全屏蒙层,提示「对方正在操作」;当 operator 变为 'local' 或 'free' 时隐藏。
蒙层应覆盖整个 VR 区域,设置 pointer-events: auto 和 cursor: not-allowed,阻止本地用户在远端操作期间进行交互。
九、第六步(可选):iframe 双端演示
在开发调试阶段,可以使用 iframe 在同一页面中同时嵌入发起方和接收方,模拟双端带看效果。
<!DOCTYPE html>
<html>
<head>
<title>带看演示</title>
<style>
#app { display: flex; height: 80vh; padding: 40px; }
#app > iframe { margin: 30px 100px; width: 1000px; }
</style>
</head>
<body>
<div id="app">
<!-- 发起方 -->
<iframe src="http://localhost:8080?m=模型ID&showtakelook=on&appid=APP_ID&userid=user_A&sig=TOKEN_A&funcuserid=user_B"></iframe>
<!-- 接收方(初始为空,由 postMessage 动态设置) -->
<iframe id="iframe2" src=""></iframe>
</div>
<script>
window.addEventListener('message', function(event) {
if (event.data.msg === 'MessageFromIframePage') {
var data = event.data.data
var frame = document.querySelector('#iframe2')
frame.contentWindow.location.href =
'http://localhost:8080?m=模型ID&takelook=on' +
'&appid=' + data.appid +
'&userid=' + data.userid +
'&sig=' + data.sig +
'&funcuserid=' + data.funcuserid
}
})
</script>
</body>
</html>
流程:
1. 左侧 iframe 加载发起方页面,用户点击「带看」按钮
2. 发起方通过 postMessage 将对方的 IM 凭证发送给父页面
3. 父页面监听 message 事件,为右侧 iframe 拼接接收方 URL 并加载
4. 双方 IM 连接成功后自动完成握手,开始实时同步
十、API 速查表
10.1 URL 参数
| 参数 | 值 | 说明 |
|---|---|---|
showtakelook |
on |
发起方:显示带看入口按钮,正常启动 |
takelook |
on |
接收方:以 guid 模式启动,自动初始化 IM |
appid |
字符串 | IM 服务的 AppKey |
userid |
字符串 | 当前用户的 IM 账号 |
sig |
字符串 | 当前用户的 IM 鉴权 Token |
funcuserid |
字符串 | 对方用户的 IM 账号 |
10.2 housePlay 方法(带看相关)
| 方法 | 说明 |
|---|---|
housePlay.start('guid') |
以引导模式启动(接收方使用),等待远端同步 |
housePlay.getCurrentState() |
获取当前 3D 坐标与角度(用于初始同步) |
housePlay.firstSyncRemoteActions(state) |
首次同步远端视角(收到 initstate 时调用) |
housePlay.syncRemoteActions(actions) |
实时同步远端操作(收到 CORE_OPERATION 时调用) |
housePlay.setConnectionStatus(true) |
设置带看连接状态为已连接 |
housePlay.setValidOperator(operator) |
设置操作权限:'local' / 'remote' / 'free' |
housePlay.warpToScene(scene) |
跳转到指定场景(收到 switchScene 时调用) |
housePlay.gotoScenebyId(index) |
按索引跳转场景(本地主动切换时调用) |
10.3 housePlay 事件(带看相关)
| 事件 | 回调参数 | 说明 |
|---|---|---|
LocalProgramLoadingCompleted |
无 | SDK 资源加载完成,应发送 initdone 开始握手 |
uploadLocalActions |
data |
本地相机操作数据,需通过 IM 发送给对方 |
firstSyncActionsCompleted |
无 | 首次视角同步完成,应设置连接状态并通知对方 |
validOperatorChanged |
operator |
操作权限变化:'remote' 时显示蒙层,其他时隐藏 |
10.4 消息类型速查
| 消息类型 | data.type | 用途 |
|---|---|---|
state |
connectState |
连接初始化握手(initdone → initstate → initstatedone) |
operation |
CORE_OPERATION |
实时相机视角同步 |
operation |
UI_OPERATION |
UI 操作同步(场景切换、标尺等) |
十一、常见问题与注意事项
-
IM 连接时机与 initdone 补发
发起方在用户点击按钮后才建立 IM,此时LocalProgramLoadingCompleted可能已触发。IM 连接成功后必须检查并补发initdone,否则握手无法完成。 -
start('guid') 与 start() 的区别
接收方必须使用start('guid')以引导模式启动,SDK 不会自动漫游而是等待远端同步视角。发起方使用start()正常启动。若接收方误用start(),双方视角会不一致。 -
userid 与 funcuserid 互换
发起方的funcuserid就是接收方的userid,反之亦然。业务后端生成 URL 时需注意正确配对。 -
操作权限管理
SDK 通过validOperatorChanged事件自动管理操作权限。当远端正在操作时,本地应显示蒙层阻止交互。不要手动调用setValidOperator来覆盖 SDK 的自动管理,除非在收到远端操作消息时需要将权限设为'remote'。 -
标尺同步
在带看模式下切换标尺时,除了调用 SDK 的enablePanoramaSize/disablePanoramaSize控制本地显示外,还需通过rulerOperation发送 UI_OPERATION 消息同步给对方。 -
场景列表数据源
场景列表优先从housePlay.settings.nestscenes.scenes获取(新版数据),若不存在则回退到housePlay.model.heroLocations(旧版数据)。两者都需过滤掉name === '总述'和script === 1的项。 -
NIM 消息格式
网易云信 NIM 使用自定义系统消息(sendCustomSysMsg),消息内容需要包装为{ type: 'VRContent', VRContent: JSON.stringify(业务数据) }格式。接收时需要两次 JSON.parse。若替换为其他 IM,消息封装格式可自行定义,只需保证解析后为TakeLookMessage即可。 -
postMessage 安全性
iframe 嵌入场景中使用postMessage通知父页面时,生产环境建议将'*'替换为具体的 origin,避免安全风险。
十二、本仓库实现参考
若需参考本仓库的具体实现,可查阅以下文件:
| 文件 | 说明 |
|---|---|
src/types/takelook.ts |
带看消息协议类型定义 |
src/stores/takelook.ts |
带看 Store:状态管理、事件注册、IM 初始化、消息收发 |
src/plugins/im/types.ts |
IM 适配器接口定义 |
src/plugins/im/BaseIM.ts |
IM 适配器基类(基于 EventEmitter) |
src/plugins/im/NIM/nim-adapter.ts |
网易云信 NIM 适配器实现 |
src/plugins/im/TIM/tim-adapter.ts |
腾讯云 TIM 适配器(预留,暂未实现) |
src/plugins/im/index.ts |
IM 插件入口,导出适配器创建函数 |
src/modules/pc/takelook/NestTakelook.vue |
带看模块根组件,编排按钮、蒙层、面板 |
src/modules/pc/takelook/NestTakeLookBtn.vue |
带看入口按钮组件 |
src/modules/pc/takelook/NestTakeLookPanel.vue |
带看场景列表面板组件(支持拖拽滚动) |
src/modules/pc/takelook/NestTakeLookOverlay.vue |
「对方正在操作」蒙层组件 |
src/stores/vr.ts |
VR Store 中带看模式的判断与启动逻辑 |
src/stores/toolbar.ts |
工具栏 Store 中标尺同步带看的逻辑 |
src/stores/url-params.ts |
URL 参数解析(含带看相关参数) |
takelook-demo.html |
iframe 双端演示页面 |