139 lines
6.6 KiB
Markdown
139 lines
6.6 KiB
Markdown
# 组件实时协议
|
||
|
||
当前协议版本为 `1`。后端的权威定义在 `domain.rs` 的
|
||
`ComponentMessage`,浏览器渲染器只依赖本文列出的稳定字段。
|
||
|
||
## 连接地址与认证
|
||
|
||
组件流地址:
|
||
|
||
```text
|
||
wss://danmaku.luoxingci.com/api/v1/components/<publicId>/stream
|
||
```
|
||
|
||
OBS 页面地址中的 token 位于 fragment:
|
||
|
||
```text
|
||
https://danmaku.luoxingci.com/obs/<publicId>#token=<component-token>
|
||
```
|
||
|
||
fragment 不会进入首次 HTTP 请求或 Nginx access
|
||
log。WebSocket 建立后,客户端必须在 8 秒内发送第一帧:
|
||
|
||
```json
|
||
{
|
||
"type": "authenticate",
|
||
"token": "component-token"
|
||
}
|
||
```
|
||
|
||
服务端验证 token 摘要、组件归属和 `events:subscribe` scope。认证成功后返回:
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"type": "authenticated",
|
||
"componentId": "08d31d19-3e4b-4c11-9cf7-9bd786a39465",
|
||
"componentKind": "song_request",
|
||
"language": "zh-CN"
|
||
}
|
||
```
|
||
|
||
之后立即发送通用的 `component.settings.snapshot`,再发送该 kind 的状态快照和实时事件。弹幕姬还会发送
|
||
`overlay.settings.snapshot` 兼容帧。token 无效、被轮换或属于其他组件时,服务端以 policy
|
||
close 结束连接。
|
||
|
||
`language` 是组件所属账户的 locale。用户在控制台修改语言后,所有已连接组件会立即收到
|
||
`component.language.updated`,payload 为 `{ "language": "en-US" }`;新连接以认证帧为准。
|
||
|
||
设置快照中的 `themeId` 是稳定的主题标识。弹幕姬与点歌姬支持 `jade-scroll` 和
|
||
`moonlit-water`;礼物特效支持 `jade-starfall` 和 `moonlit-water`;礼物菜单支持 `jade-banquet` 和
|
||
`moonlit-water`。消费者应把未知主题降级为自身默认主题,不能因主题发布顺序不同而中断实时消息。
|
||
|
||
## 版本化事件信封
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"id": "d56c1a28-a6f1-4ad9-b02f-4c22dc4d3c27",
|
||
"componentId": "08d31d19-3e4b-4c11-9cf7-9bd786a39465",
|
||
"sourceId": "c8087bb0-0dde-40eb-a43d-a9a2574c2717",
|
||
"occurredAt": "2026-07-15T20:10:30.125Z",
|
||
"roomId": "123456",
|
||
"type": "live.danmaku",
|
||
"payload": {
|
||
"viewer": { "uid": "42", "name": "观众" },
|
||
"text": "晚上好",
|
||
"segments": [{ "type": "text", "text": "晚上好" }]
|
||
}
|
||
}
|
||
```
|
||
|
||
`ownerId` 永远不会序列化到浏览器。消费者应按 `version` 和 `type`
|
||
分派,并忽略不认识的 payload 字段,从而允许兼容地增加元数据。 `sourceId`
|
||
标识所属账户唯一的直播监听连接,不表示组件单独绑定了一个直播源;同一账户不同组件收到的实时事件具有相同的
|
||
`sourceId`。
|
||
|
||
## 事件类型
|
||
|
||
| `type` | 主要 payload | 说明 |
|
||
| ----------------------------- | --------------------------------------------- | ---------------------- |
|
||
| `component.settings.snapshot` | `settings` | 所有组件的设置快照 |
|
||
| `component.settings.updated` | `settings` | 所有组件的设置更新 |
|
||
| `component.language.updated` | `language` | 账户语言实时更新 |
|
||
| `overlay.settings.snapshot` | `settings` | 认证后当前设置快照 |
|
||
| `overlay.settings.updated` | `settings` | 控制台保存后的实时设置 |
|
||
| `live.danmaku` | `viewer`, `text`, `segments` | 普通文字和表情分段 |
|
||
| `live.enter` | `viewer` | 进房事件 |
|
||
| `live.gift` | `viewer`, `gift`, `quantity`, `sourceEventId` | 一次可记账的礼物事件 |
|
||
| `live.gift.combo` | `viewer`, `gift`, `quantity`, `comboId` | 同一连击的视觉更新 |
|
||
| `live.superchat` | `viewer`, `message`, `price`, `sourceEventId` | 醒目留言 |
|
||
| `live.guard.buy` | `viewer`, `guardName`, `quantity`, `price` | 舰长购买 |
|
||
| `live.like` | `viewer` | 点赞 |
|
||
| `live.share` | `viewer` | 分享 |
|
||
| `live.unknown` | `command`, `metadata` | 有界且已清理的未知事件 |
|
||
|
||
## 点歌姬状态
|
||
|
||
`song_request` 认证后按顺序接收:
|
||
|
||
1. `song.queue.snapshot.begin`:包含 `snapshotId`、`revision`、当前歌曲和待唱总数。
|
||
2. 零到多个 `song.queue.snapshot.page`:每页最多 100 个待唱项。
|
||
3. `song.queue.snapshot.end`:提交该快照。
|
||
|
||
后续 `song.queue.changed` 携带连续 revision,`operation` 为 `added`、`promoted`、
|
||
`completed`、`cancelled` 或
|
||
`rating-updated`。客户端发现 revision 缺口必须重连取得新快照,不能猜测缺失队列状态。
|
||
|
||
礼物目录价格的原始单位是人民币的千分之一。`gift.totalPrice` 保留该整数单位,`gift.priceCny`
|
||
是供展示使用的人民币数值。
|
||
|
||
`live.gift` 与 `live.gift.combo` 不是两笔礼物。需要持久化计数的组件通常只消费
|
||
`live.gift`;连击事件用于更新同一张视觉卡片。
|
||
|
||
`gift_effect` 只订阅 `live.gift` 与 `live.guard.buy`。礼物档位由 sanitized settings 中的
|
||
`highValueThreshold` 和 `featuredValueThreshold` 决定;浏览器使用 `gift.imageUrl` 或
|
||
`gift.animationUrl`
|
||
作为流星主体,图片失效时必须使用本地星光占位。该组件不维护状态快照,重连后只展示新到达的实时事件。
|
||
|
||
`gift_menu` 同样只消费一次性 `live.gift` 与 `live.guard.buy`。命中配置后投影为
|
||
`gift-menu.triggered`,payload 包含 `itemIds`、`viewer` 和
|
||
`sourceEventId`。一个特定礼物和一个同价电池规则可以同时命中多个菜单项;客户端应全部高亮,并滚动到第一个匹配项。未命中的投喂不会进入该组件通道。
|
||
|
||
## 表情分段
|
||
|
||
`live.danmaku.payload.segments` 是判别联合:
|
||
|
||
- `text`:包含可直接显示的文字。
|
||
- `emoticon`:包含回退文字、图片 URL、可选尺寸、动态标记和整条大表情标记。
|
||
|
||
图片失败时客户端必须回退到 `text`,不能让一张失效图片破坏整条弹幕。
|
||
|
||
## 背压与重连
|
||
|
||
- 每个组件使用有界 `tokio::broadcast` channel。
|
||
- 慢客户端发生 `Lagged` 时跳过已经丢失的旧帧,继续接收新事件;实时展示不保证历史重放。
|
||
- 浏览器对非鉴权关闭使用指数退避重连,上限 12 秒。
|
||
- 鉴权失败不会自动重试,避免对已撤销 token 形成无限请求。
|
||
- 需要可靠业务处理的功能必须实现 `EventHandler` 并写入数据库,不能把 WebSocket 当作消息队列。
|