# Live.js

[![npm version](https://img.shields.io/npm/v/@realsee/live.svg?style=flat-square&logo=npm&label=npm%20install%20@realsee/live)](https://www.npmjs.com/package/@realsee/live)

## 功能说明

**Live** 是 [如视开放平台线上带看](https://open-platform.realsee.com/developer/docs/live/intro/) 的核心客户端 Web SDK，搭配 Live Container
SDKs（iOS/Android/微信小程序）和 Live API 可以快速实现实时同屏、实时语音、IM 等功能。

### 名词释义

- **帧数据(Keyframe)**: 指用户界面需同屏交互信息的状态数据的集合。

> 通常以键值对的形式表现——键为界面模块名字，值为该模块的状态信息。  
> 比如帧数据 `{"popupPanel": { hidden: true }}` 表示界面交互中有个弹窗面板，我们命名为`popupPanel`，它有个`hidden`状态，`true`表示其正处于隐藏状态。

- **快照(Snapshot)**: 指某个时刻所有 **帧数据** 的集合。

> 您可以理解 **帧数据** 是 **快照** 的子集，通过某时刻的"快照"信息能够还原该时刻的用户界面所有状态。

- **RTC**: Real-Time Communication, 在本文中则特指渲染 HTML 页面的 WebView/Browser 等容器的音视频实时通信能力。

> 需注意的是 **Live** 并不包含 **RTC** 功能的实现，需借助客户端原生能力的支持。更多细节详见后文 [RTC 协议](#rtc-协议) 部分。

## 快速上手

### 安装 SDK

可以通过 Node 包管理工具 [npm](https://www.npmjs.com/) 或 [yarn](https://yarnpkg.com/) 安装**Live**：

```bash
# npm
npm i @realsee/live
# yarn
yarn add @realsee/live
```

### 如何使用

简单使用样例（先不考虑实时语音能力）：

```typescript
import { createLive } from '@realsee/live'

// 定义帧数据类型（可选，但推荐使用 TypeScript）
interface MyKeyframes {
  camera: { position: [number, number, number] }
  annotation: { text: string; position: [number, number] }
}

const live = createLive<MyKeyframes>({
  url: 'wss://ws-access.realsee.com/***/?=xxx' /* 连接 WebSocket 服务的 URL */,
  getTicket: async () => {
    /* 获取带看"门票"的回调函数 */
    const response = await fetch('/api/get-ticket')
    return response.json().ticket
  },
})

// 连接带看
await live.connect()
```

在创建 `live` 实例时配置了两个参数：

- `url`：连接 WebSocket 服务的 URL（可选），如果在创建 `live` 实例阶段并不知道 URL，您可以在合适的时机通过 `live.connect({ url })` 方法进行 WebSocket 连接。
- `getTicket()`：是一个异步回调函数（可选），其返回值是连接 WebSocket 服务的 **"门票"**，关于如何获取 `ticket` 请查阅 [如视开放平台带看文档](https://open-platform.realsee.com/developer/docs/live/intro/)。也可以在 `connect` 时提供。

> **提示**：如果在创建实例时同时提供了 `url` 和 `getTicket`，Live 会自动尝试连接，无需手动调用 `connect()` 方法。

> 关于 `createLive()` 函数的更多参数配置请参考 [LiveOptions](https://unpkg.com/@realsee/live/docs/interfaces/live.LiveOptions.html) 文档。

获取实例 `live` 之后，可以做很多有趣的事情。但在讲述详细用途之前，先简单介绍下 **Live** 中的 **消息系统**。

## 消息系统

**Live** 核心职责是借助 WebSocket 长链双工通信能力实现多端用户的同屏连线。用户间的通信都是基于 WebSocket 消息。

**Live** 将消息分为`帧数据`、`广播`、`房间信息`三类。针对每一类信息提供相应的发送、接收 API，更多 API
细节请参考 [Live API 文档](https://unpkg.com/@realsee/live/docs/index.html) 。

**帧数据** 是进行同屏的信息载体，由用户自行定义当前场景中哪些信息用以同屏，将这些信息组织成 `{key: value}` 的形式即可。

**广播消息** 用以房间成员之间以事件的形式指定某个用户向其发送自定义广播消息。如：主持人向观众发送问候消息。

**房间信息** 为内置消息体，由 WebSocket 服务主动推送，包括房间状态、当前用户及参与用户列表等。部分信息也可通过 **Live** 提供的更新方式进行修改。

### 帧数据同步

当前 UI 交互状态的同步：

以 [Five](https://developers.realsee.com/docs/#/docs/five/web/quickstart) 同屏为例

```typescript
// 发送本地 帧数据 (Five State)
live.sendKeyframe('five', { panoIndex: 13 })

// 接收远程 帧数据 (Five State)
live.keyframes.on('five', (newState, prevState, frontRequestId) => {
  // 可以拿最新的 `newState` 更新本地 UI 状态
})
```

您也可以通过 `live.snapshot` 属性来获取当前所有序列帧数据的"快照"。

### 房间信息

**Live** 有会话房间的概念，当您成功连接 WebSocket 服务之后等同于您作为一个用户加入了一个房间。此房间用户除了您之外还会有其他用户，您可以通过如下方式获取或更新用户信息：

```typescript
// 属性：获取本地用户信息
live.selfInfo

// 属性：获取当前房间内所有用户列表
live.userList

// 属性：获取房间信息
live.roomInfo

// 方法：更新本地用户信息
live.setSelfInfo({})

// 事件：本地用户信息发生变更
live.on('selfInfoUpdate', (userInfo, frontRequestId) => {})

// 事件：房间用户列表信息发生变更
live.on('userListUpdate', (userList, frontRequestId) => {})
```

### 广播消息

除了帧数据和房间信息之外，还可以用事件的形式向房间内的其他用户广播消息：

```typescript
live.broadcast(
  data /* 广播的数据 */, 
  ['user_id_1', 'user_id_2'] /* 收到此消息的用户ID列表 */,
  0 /* 超时时间（毫秒），0 表示不超时 */
)
```

此时，房间内用户 Id 为 `user_id_1`、`user_id_2` 的用户会收到此广播消息（其他用户无感知）：

```typescript
live.on('broadcast', (data /* 广播的数据 */, frontRequestId) => {})
```

更多的例子请查看 [Live API](https://unpkg.com/@realsee/live/docs/index.html) 文档 。

## 事件监听

**Live** 提供 `带看事件` 和 `RTC 事件` 两类异步事件的监听。

所有事件都可以通过 `live.on('/*事件名*/', /*事件回调*/)` 的方式监听。

比如监听连接状态变化：

```typescript
live.on('stateChange', (state, prevState) => {
  console.log(`状态从 ${prevState} 变为 ${state}`)
})
```

### 带看连接状态

**Live** 前后端服务交互是基于 WebSocket 长链接的。一次带看成功进入、退出的标识由连接状态 `OPEN`、`CLOSED` 体现。因此，所有 **VRTC** 相关的事件处理逻辑应保证在 `OPEN` 状态之后进行。

**连接状态枚举表：**

| 状态名称       | 状态描述   |
| -------------- | ---------- |
| NOTINITIALIZED | 未被初始化 |
| CONNECTING     | 正在连接中 |
| OPEN           | 连接成功   |
| CLOSING        | 关闭中     |
| CLOSED         | 关闭成功   |

### 带看事件

- `stateChange(state: LiveState, prevState?: LiveState): void` - 带看连接状态变化事件

- `builtinEvent(builtinMsg: BuiltinMsg): void` - 接收服务端推送的内置事件消息（包括用户变化和房间变化）

- `userChange(builtinMsg: BuiltinMsg): void` - 用户信息改变事件（是 builtinEvent 的子集，event_type 为 UserChange）

- `roomChange(builtinMsg: BuiltinMsg): void` - 房间信息改变事件（是 builtinEvent 的子集，event_type 为 RoomChange）

- `broadcast(evtMsg: Record<string, any>, frontRequestId: string): void` - 接收其他用户的广播消息

- `keyframes(keyframes: Partial<Snapshot>, frontRequestId: string): void` - 接收其他用户的帧数据

- `readyKeyframeSync(lastKeyframe: Partial<Snapshot>): void` - 表明帧同步行为已就绪（**已废弃**，请使用 `keyframes` 事件）

- `selfInfoUpdate(userInfo: UserInfo, frontRequestId: string): void` - 自身的信息发生变更

- `userListUpdate(userList: UserInfo[], frontRequestId: string): void` - 用户列表信息发生变更

- `kickOut(): void` - 被踢出房间

- `error(liveMsg: LiveMsg): void` - 服务端推送出现异常

`live` 实例提供了很多诸如`live.connect()`、`live.broadcast()`、`live.sendKeyframe()`、`live.exit()`、`live.close()`、`live.dispose()`等方法，这些方法都是异步反馈且可能会影响房间内的其他用户。
您可以通过监听以上事件来获知这些影响。

### 主要方法说明

- `connect(options?)` - 连接带看房间，建立 WebSocket 连接
- `exit()` - 退出带看房间（不发送房间关闭消息）
- `close()` - 关闭带看房间（会发送房间关闭消息，通知服务器）
- `dispose()` - 销毁带看实例，清理所有资源（事件监听、Yjs 文档等）
- `sendKeyframe(key, frame)` - 发送帧数据
- `getFramesByKey(key)` - 根据键获取对应的帧数据
- `broadcast(data, toUserIds, timeout?)` - 向指定用户广播消息
- `setSelfInfo(userInfo)` - 更新本地用户信息
- `toggleMicro(status)` - 切换麦克风状态
- `kick(toUserIds)` - 踢出指定用户
- `forbidMicro(status, toUserIds?)` - 禁止/允许指定用户的麦克风
- `forbidSync(sync, toUserIds?)` - 禁止/允许指定用户的帧数据同步
- `updateStatus(evtType, atnType, status, toUserIds?)` - 更新带看状态（用户或房间状态）

### 主要属性说明

- `state` - 当前带看连接状态（LiveState）
- `readyState` - WebSocket 就绪状态
- `url` - WebSocket 连接 URL
- `options` - 配置选项（只读）
- `snapshot` - 当前所有帧数据的快照
- `selfInfo` - 本地用户信息
- `userList` - 房间内所有用户列表
- `roomInfo` - 房间信息
- `$RTC` - RTC 实例（用于语音通信）
- `jsBridge` - JSBridge 实例（用于 App/WebView 环境）
- `keyframes` - 帧数据监听器（Subscribe 实例）

### RTC 事件

RTC 相关事件通过 `live.$RTC.on('/*事件名*/', /*事件回调*/)` 方式来监听。

- `error(error: Error): void` - 异常事件

- `initWillStart(): void` - 即将初始化事件

- `inited(): void` - 初始化完成事件

- `joinWillStart(): void` - 即将加入语音房间事件

- `joined(info: RTCJoinedInfo): void` - 成功加入语音房间事件

- `userVolumes(userVolumes: UserVolume[]): void` - 房间内每个说话用户的音量

- `weakNetwork(): void` - 弱网提醒事件

比如监听语音房间内正在说话的用户及其音量：

```typescript
live.$RTC.on('userVolumes', (userVolumes: UserVolume[]) => {
  console.log(userVolumes) // userVolumes 正在说话的用户及其音量数据。
})
```

### 异常处理

- `error(liveMsg: LiveMsg): void` - 服务端推送出现异常。

与 WebSocket 通信的结构体`LiveMsg`格式如下：

| 名称            | 类型                | 描述                                                                                                  |
| --------------- | ------------------- | ----------------------------------------------------------------------------------------------------- |
| appId           | string              | 应用ID                                                                                                 |
| code            | string              | 返回码，详情参照[如视开放平台带看文档](https://open-platform.realsee.com/developer/docs/live/intro/) |
| command         | 指令                | WebSocket 通信指令类型，详情请查看[Live API](https://unpkg.com/@realsee/live/docs/index.html) 文档    |
| data            | Record<string, any> | 本条指令对应的业务数据                                                                                |
| frontRequestId  | string              | 前端的请求 ID，由 WS 服务原样透传回前端                                                               |
| message         | string              | 返回描述                                                                                              |
| requestId       | string              | 后端请求 ID                                                                                           |
| roomCode        | string              | 带看房间号                                                                                            |
| triggerUserId   | string              | 触发指令的用户 ID                                                                                     |

当 WebSocket 推送的数据结构体不满足 `LiveMsg` 或者 `code` 值不为 `SUCCESS` 时会以 `error` 事件的形式抛出来。

- `error(error: Error): void` - RTC 语音出现异常。

> RTC 语音不同容器或 RTC 解决方案不同，其错误信息内容可能不完全一致。

## RTC 协议

语音能力需依赖 WebView/Browser 容器，即需要容器端实现 RTC 能力。

为了方便开发者接入，**如视开放平台** 为主流平台提供 [容器 SDK](https://open-platform.realsee.com/developer/docs/live/intro/)
和 [jsbridge-x](https://www.npmjs.com/package/@realsee/jsbridge-x) 。

**Live** 通过 **jsbridge-x** 与 集成了**容器 SDK** 的客户端应用或微信小程序应用程序桥接，实现原生能力的调用。这个 `jsbridge-x`
实例需作为配置参数提供给`createLive()`。

如果是 iOS/Android App（已接入如视 VRTC 容器 SDK）

```typescript
import JSBridge from '@realsee/jsbridge-x'
import { createLive, VRWebViewRTC } from '@realsee/live'

const jsBridgeInstance = new JSBridge()

const rtcInstance = new VRWebViewRTC({
  jsBridge: jsBridgeInstance,
  getVoiceSign: async (params) => {
    // 获取语音签名
    const response = await fetch('/api/get-voice-sign', {
      method: 'POST',
      body: JSON.stringify(params)
    })
    return response.json()
  },
})

const live = createLive({
  rtc: rtcInstance,
  getTicket: async () => {
    const response = await fetch('/api/get-ticket')
    return response.json().ticket
  },
})
```

提供 RTC 实例配置参数之后，语音的连接、断开、重连等逻辑就托管给了 **Live** 。您可以通过 live 实例上的 `$RTC` 命名空间访问语音相关的状态和事件。

```typescript
// 状态：用户是否成功加入语音房间
live.$RTC.joined
// 状态：用户麦克风状态
live.$RTC.micro
// 状态：当前语音房间ID
live.$RTC.voiceId
// 状态：RTC 类型
live.$RTC.type
// 监听用户音量变化
live.$RTC.on('userVolumes', (userVolumes) => {})
```

## 浏览器端支持

不同于客户端(Android\iOS) 已经内置 **RTC** 能力，浏览器端需要我们自己安装 第三方 **RTC SDK** 依赖。 **Live** 内置 腾讯[TRTC](https://github.com/tencentyun/TRTCSDK/blob/HEAD/Web/README-zh_CN.md) 和 亚马逊[Chime](https://github.com/aws/amazon-chime-sdk-js) 两个语音能力集成实现。

> 纯浏览器端不需要 **Native** 中转能力，没有 **jsBridge** 依赖。

### TRTC

安装依赖：

```shell
# 需要您自行安装
yarn add trtc-js-sdk
# 或
npm install trtc-js-sdk
```

使用方式：

```typescript
import { createLive, BrowserRTC } from '@realsee/live'

const rtcInstance = new BrowserRTC({
  getVoiceSign: async (params) => {
    const response = await fetch('/api/get-voice-sign', {
      method: 'POST',
      body: JSON.stringify(params)
    })
    return response.json()
  },
  // 可选：处理自动播放策略
  // autoPlayConfirm: ({ onConfirm }) => showDialogThen(onConfirm)
})

const live = createLive({
  rtc: rtcInstance,
  getTicket: async () => {
    const response = await fetch('/api/get-ticket')
    return response.json().ticket
  },
})
```

### Chime

安装依赖：

```shell
# 需要您自行安装
yarn add amazon-chime-sdk-js
# 或
npm install amazon-chime-sdk-js
```

使用方式：

```typescript
import { createLive, BrowserRTC4Chime } from '@realsee/live'

const rtcInstance = new BrowserRTC4Chime({
  getVoiceSign: async (params) => {
    const response = await fetch('/api/get-voice-sign', {
      method: 'POST',
      body: JSON.stringify(params)
    })
    return response.json()
  },
})

const live = createLive({
  rtc: rtcInstance,
  getTicket: async () => {
    const response = await fetch('/api/get-ticket')
    return response.json().ticket
  },
})
```

### 使用内置的 JsBridge 和 RTC 实现

使用方式：

```typescript
import { createLive, BuiltInRTCType } from '@realsee/live'

const live = createLive({
  useBuiltInRTC: true, // 使用内置rtc
  builtInRTCType: BuiltInRTCType.RealseeXChimeRTC, // 内置 rtc 类型，不填则默认trtc，chime请指明
  useBuiltInJsBridge: true, // 使用内置的jsBridge
  getVoiceSign: async (params) => {
    // 内置的rtc还是要把 getVoiceSign 方法传递进去
    const response = await fetch('/api/get-voice-sign', {
      method: 'POST',
      body: JSON.stringify(params)
    })
    return response.json()
  },
  getTicket: async () => {
    const response = await fetch('/api/get-ticket')
    return response.json().ticket
  },
})

// 可以直接使用里面的 jsBridge, 该实例由 @realsee/jsbridge-x 提供支持
const jsBridge = live.jsBridge
jsBridge.ready().then(() => console.info(`${jsBridge.webViewType} jsBridge is ready`))
```

## TypeScript & React 支持

**Live** 有完整的 [TypeScript Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html)
和 [React Hooks](https://reactjs.org/docs/hooks-intro.html) 支持，如果您的技术栈基于 `TypeScript` 和 `React` 可以参考如下方式更优雅地使用 **Live**。

![TypeScript效果演示](https://vrlab-static.ljcdn.com/release/web/tssupportminify.f2eaeed2.png)

步骤一：新建文件 `LiveReact.ts` 并创建您自己的 `LiveReact` 实例。

```typescript
import { createLiveReact } from '@realsee/live'

/** 定义帧数据快照结构 */
interface Snapshot {
  /** 以 Five 状态快照为例 */
  five: {
    panoIndex: number
    fov: number
    mode: 'Panorama' | 'Model'
    latitude: number
    longitude: number
  }
}

/** 通过 `createLiveReact()` 函数创建实例*/

const LiveReact = createLiveReact<Snapshot>({
  getTicket: async () => {
    const response = await fetch('/api/get-ticket')
    return response.json().ticket
  },
  getVoiceSign: async (params) => {
    const response = await fetch('/api/get-voice-sign', {
      method: 'POST',
      body: JSON.stringify(params)
    })
    return response.json()
  },
  useBuiltInRTC: true,
})

export default LiveReact

/** 如果您不喜欢 `LiveReact.LiveProvider` 这类编程习惯，可以将需要的函数"抛"出来 */
export const LiveProvider = LiveReact.LiveProvider
export const useConnect = LiveReact.useConnect
export const useKeyframe = LiveReact.useKeyframe
export const useLiveAction = LiveReact.useLiveAction
export const useLiveState = LiveReact.useLiveState
export const useUserList = LiveReact.useUserList
// export ...
```

步骤二：通过 `LiveProvider` 将 `live` 实例集成到 `React` 的 [Context](https://reactjs.org/docs/context.html) 上下文环境中。

```tsx
import { LiveProvider } from './LiveReact'

ReactDOM.render(
  <LiveProvider>
    <RootApp />
  </LiveProvider>,
  container,
)
```

步骤三：在您的 `React` 函数组件中使用相关 `Hooks` 函数。

### 可用的 React Hooks

**Live** 提供了丰富的 React Hooks，方便在 React 组件中使用：

- `useLiveInstance()` - 获取当前 Live 实例
- `useRTCInstance()` - 获取当前 RTC 实例
- `useConnect()` - 获取连接函数
- `useKeyframe(key)` - 获取指定键的帧数据状态和更新函数（类似 useState）
- `useKeyframeUpdateCallback(key, callback, deps)` - 监听指定键的帧数据更新回调
- `useLiveAction()` - 获取带看操作函数（connect, sendKeyframe, toggleMicro, broadcast, exit, kick, forbidMicro, forbidSync, getFramesByKey, setSelfInfo, updateStatus）
- `useLiveEventCallback(name, callback, deps)` - 监听带看事件
- `useLiveState()` - 获取带看连接状态
- `useRTCAction()` - 获取 RTC 操作函数（join, quit, toggleMicro, detectMicro, shock）
- `useRTCEventCallback(name, callback, deps)` - 监听 RTC 事件
- `useSelfInfo()` - 获取和更新用户信息（类似 useState）
- `useKeyframesSnapshot()` - 获取所有帧数据快照
- `useUserList()` - 获取用户列表

使用示例：

```tsx
import { useConnect, useLiveAction, useKeyframe, useLiveState, LiveState } from './LiveReact'

function ReconnectBtn() {
  const connect = useConnect()
  const liveState = useLiveState()

  return (
    <button 
      onClick={() => connect({ 
        url: 'wss://example.com/live',
        getTicket: async () => {
          const response = await fetch('/api/get-ticket')
          return response.json().ticket
        }
      })}
      disabled={liveState === LiveState.CONNECTING || liveState === LiveState.OPEN}
    >
      {liveState === LiveState.OPEN ? '已连接' : '重新连接'}
    </button>
  )
}

function CameraControl() {
  const [camera, setCamera] = useKeyframe('camera')
  const { sendKeyframe } = useLiveAction()

  const handleMove = () => {
    const newCamera = { position: [1, 2, 3], rotation: [0, 0, 0] }
    setCamera(newCamera) // 会自动同步到其他用户
    // 或者使用 sendKeyframe
    // sendKeyframe('camera', newCamera)
  }

  return (
    <div>
      <p>相机位置: {camera?.position?.join(', ')}</p>
      <button onClick={handleMove}>移动相机</button>
    </div>
  )
}
```

## Live API 文档

欢迎查看 [Live API](https://unpkg.com/@realsee/live/docs/index.html) 文档 。  
该文档由 [TypeDoc](https://typedoc.org/) 生成，您可以详细查看 API 使用方式、事件 Hooks 及数据结构定义等。

## 相关文档链接

- [@realsee/five](https://www.npmjs.com/package/@realsee/five)
- [@realsee/jsbridge-x](https://www.npmjs.com/package/@realsee/jsbridge-x)
- [如视开放平台线上带看](https://open-platform.realsee.com/developer/docs/live/intro/)
