首次推送
This commit is contained in:
@@ -0,0 +1,452 @@
|
||||
# 流程文档总览
|
||||
|
||||
> 本文档按用户流程组织,将 SDK 技术行为拆分为七个主流程文件和一个独立的状态同步流程文件。
|
||||
> 依据:`docs/sdk-bridge-api.md`
|
||||
> SDK:`github.com/honeybbq/teamspeak-go` → gomobile AAR → `TSBridge.kt` 应用层桥接
|
||||
|
||||
---
|
||||
|
||||
## 一、完整用户流程
|
||||
|
||||
```
|
||||
进入应用
|
||||
│
|
||||
├─ 已有最近服务器? ──是──→ 显示最近服务器列表 ──→ 选择服务器
|
||||
│ │
|
||||
└─ 否 ──→ 输入服务器地址 ──→ 输入昵称 ──→ 输入密码(可选) ──→┘
|
||||
│
|
||||
点击"进入服务器"
|
||||
│
|
||||
┌─────────────────────┘
|
||||
▼
|
||||
连接中(显示进度)
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
连接成功 连接失败 超时
|
||||
│ │ │
|
||||
│ 显示错误原因 显示超时提示
|
||||
│ 允许重试 允许重试
|
||||
│ │ │
|
||||
│ └───────→ 返回输入页面
|
||||
▼
|
||||
分配到默认频道(首次同步 ①)
|
||||
│
|
||||
▼
|
||||
浏览频道列表 ──→ 浏览频道中的人员分布
|
||||
│ │
|
||||
│ poke 服务器内成员
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──→ 进入/切换频道 ←──────────┘
|
||||
│ │ ← 自身状态同步 ④
|
||||
│ │ ← 增量同步 ② / 补偿同步 ③
|
||||
│ │
|
||||
│ ├─ 频道有密码? ──是──→ 输入密码
|
||||
│ │
|
||||
│ ├─ 收听语音 ──→ 发送语音(PTT)
|
||||
│ │
|
||||
│ ├─ 收到文本消息 ──→ 消息归档同步 ⑤ ──→ 发送文本消息
|
||||
│ │
|
||||
│ ├─ 浏览频道文件 ──→ 上传文件 ──→ 下载文件
|
||||
│ │
|
||||
│ ├─ poke 服务器内成员 ──→ 被 poke
|
||||
│ │
|
||||
│ ▼
|
||||
│ 切换频道 / 留在当前频道
|
||||
│ │
|
||||
└────┘
|
||||
│
|
||||
▼
|
||||
┌─── 离开原因 ───┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
主动断开 网络异常 被踢出
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
发送 显示错误 显示被踢原因
|
||||
disconnect 允许重连 允许重连
|
||||
│ │ │
|
||||
│ 重连后全量同步 ⑥
|
||||
│ │ │
|
||||
└────────┼─────────┘
|
||||
▼
|
||||
清理会话状态
|
||||
│
|
||||
▼
|
||||
返回应用主页
|
||||
```
|
||||
|
||||
> **① 首次同步** ② **增量同步** ③ **补偿同步** ④ **自身状态同步** ⑤ **消息归档同步** ⑥ **重连后全量同步**
|
||||
> 详见 [08_状态同步.md](08_状态同步.md)
|
||||
|
||||
---
|
||||
|
||||
## 二、用户流程对应的程序流程概览
|
||||
|
||||
### 2.1 连接服务器
|
||||
|
||||
```
|
||||
读取本地配置
|
||||
│
|
||||
检查输入是否合法
|
||||
│
|
||||
创建 Identity(首次生成,后续读取)
|
||||
│
|
||||
TSBridge.connect(host, nickname, password, defaultChannel, defaultChannelPassword, callbacks)
|
||||
│
|
||||
callbacks 内部注册 EventBus.emit()(将 JNI 回调转为 EventBus 事件)
|
||||
│
|
||||
TSClient.connect() → 建立 UDP 连接 → 身份认证 → 服务器密码验证
|
||||
│
|
||||
├── 失败 → 连接失败处理 → 显示错误
|
||||
│
|
||||
└── 成功 →
|
||||
│
|
||||
EventBus 收到 Connected 事件
|
||||
│
|
||||
开始同步服务器数据
|
||||
│
|
||||
├── 获取频道列表 TSBridge.getChannelsJSON()
|
||||
├── 获取成员列表 TSBridge.getClientsJSON()
|
||||
└── 获取自身 ID TSBridge.getClientID()
|
||||
│
|
||||
建立本地状态(频道基线 + 成员基线)
|
||||
│
|
||||
UI 切换到服务器页面
|
||||
```
|
||||
|
||||
### 2.2 浏览频道与成员
|
||||
|
||||
```
|
||||
已同步的频道列表 → 构建树形结构 → 渲染频道树
|
||||
│
|
||||
已同步的成员列表 → 按 channelID 索引 → 派生每个频道的成员列表
|
||||
│
|
||||
持续接收 EventBus 事件(debounce 合并后处理):
|
||||
├── ClientEnter → 新增成员到成员表
|
||||
├── ClientMoved → 更新成员所在频道
|
||||
├── ClientLeave → 从成员表移除
|
||||
└── Poked → 显示 Poke 通知
|
||||
│
|
||||
未知 clientID → 触发 TSBridge.getClientsJSON() 补偿同步
|
||||
未知 channelID → 触发 TSBridge.getChannelsJSON() 补偿同步
|
||||
```
|
||||
|
||||
### 2.3 切换频道
|
||||
|
||||
```
|
||||
用户选择目标频道
|
||||
│
|
||||
├── 频道有密码? → 弹出密码输入
|
||||
│
|
||||
├── TsClient.clientMove(selfID, targetID, password)
|
||||
│
|
||||
├── 命令被拒绝 → 显示失败原因 → 允许重试
|
||||
│
|
||||
└── 命令成功 → 等待 onClientMoved 服务端事实
|
||||
│
|
||||
比对 clientID == selfID
|
||||
│
|
||||
提交新频道事实 → UI 导航到目标频道
|
||||
```
|
||||
|
||||
### 2.4 发送与接收文本消息
|
||||
|
||||
```
|
||||
发送:
|
||||
用户输入消息 → 选择目标模式(频道/私聊/服务器)
|
||||
│
|
||||
TSBridge.sendTextMessage(targetMode, targetId, message)
|
||||
│
|
||||
├── 失败 → 标记消息发送失败 → 允许重试
|
||||
│
|
||||
└── 成功 → 标记发送完成
|
||||
|
||||
接收:
|
||||
服务器推送 notifytextmessage
|
||||
│
|
||||
EventBus 收到 TextMessage 事件(立即分发,不合并)
|
||||
│
|
||||
按 targetMode 与 target 归档(不用当前页面猜测)
|
||||
│
|
||||
更新目标会话和未读状态
|
||||
```
|
||||
|
||||
### 2.5 语音通信
|
||||
|
||||
```
|
||||
用户按下 PTT 按钮
|
||||
│
|
||||
├── 前置条件检查(已连接 + 已在频道中)
|
||||
│ └── 不满足 → 显示无法发言原因
|
||||
│
|
||||
└── 满足 → 启动麦克风采集 + Opus 编码(Dispatchers.IO)
|
||||
│
|
||||
循环发送:
|
||||
└── 每帧 → TSBridge.sendVoice(data, codec)
|
||||
│
|
||||
├── 用户松开 → StopCapture → 恢复静默
|
||||
│
|
||||
└── 连接断开 → StopCapture → 进入 Blocked
|
||||
|
||||
接收语音(绕过 EventBus,延迟敏感):
|
||||
VoiceService.handleVoiceData() → Opus 解码 → AudioTrack 播放
|
||||
```
|
||||
|
||||
### 2.6 文件传输
|
||||
|
||||
```
|
||||
上传:
|
||||
选择文件 → FileTransferInitUpload(channelID, path, size, overwrite)
|
||||
│
|
||||
获得 host/port/key
|
||||
│
|
||||
DialFileTransfer(host, port, key) → 建立 TCP 连接
|
||||
│
|
||||
UploadFileData(host, info, reader) → 传输文件字节流
|
||||
|
||||
下载:
|
||||
选择文件 → FileTransferInitDownload(channelID, path, password)
|
||||
│
|
||||
获得 host/port/key
|
||||
│
|
||||
DialFileTransfer(host, port, key) → 建立 TCP 连接
|
||||
│
|
||||
DownloadFileData(host, info, writer) → 接收文件字节流
|
||||
```
|
||||
|
||||
### 2.7 断开连接
|
||||
|
||||
```
|
||||
主动断开:
|
||||
用户点击离开 → 状态改为 disconnecting → 阻止新操作
|
||||
│
|
||||
TSBridge.disconnect() → 通知服务器客户端主动离开
|
||||
│
|
||||
EventBus 收到 Disconnected 事件 → 停止语音和文件传输 → 清理会话状态 → 返回主页
|
||||
|
||||
被动断开:
|
||||
UDP 会话中断 → EventBus 收到 Disconnected(error) → 显示错误原因 → 允许重连
|
||||
|
||||
被踢出:
|
||||
notifyclientleftview reasonid=4/5 → EventBus 收到 Kicked(reason) → 显示被踢原因 → 允许重连
|
||||
```
|
||||
|
||||
### 2.8 状态同步(独立于主流程的横切关注点)
|
||||
|
||||
```
|
||||
贯穿所有主流程的同步机制:
|
||||
|
||||
① 首次同步(连接建立后)
|
||||
触发:EventBus 收到 Connected 事件
|
||||
动作:TSBridge.getChannelsJSON() + TSBridge.getClientsJSON() + TSBridge.getClientID()
|
||||
目标:建立完整的频道基线和成员基线
|
||||
|
||||
② 增量同步(事件驱动)
|
||||
触发:EventBus 收到 ClientEnter / ClientMoved / ClientLeave 事件
|
||||
动作:debounce 合并后 refreshClientList() 全量刷新
|
||||
目标:维护实时成员位置和频道人数
|
||||
|
||||
③ 补偿同步(数据不一致时)
|
||||
触发:事件引用了未知的 clientID 或 channelID
|
||||
动作:重新调用 TSBridge.getClientsJSON() 或 TSBridge.getChannelsJSON()
|
||||
目标:用完整列表修复丢失的基线数据
|
||||
|
||||
④ 自身状态同步(频道切换后)
|
||||
触发:EventBus 收到自己的 ClientMoved 事件
|
||||
动作:比对 clientID == selfID → 更新自身所在频道
|
||||
目标:确认当前用户的实际频道位置
|
||||
|
||||
⑤ 消息归档同步(收到消息时)
|
||||
触发:EventBus 收到 TextMessage 事件
|
||||
动作:按 targetMode + target 归档到正确的会话
|
||||
目标:维护消息与目标频道的对应关系
|
||||
|
||||
⑥ 重连后全量同步(断开重连后)
|
||||
触发:重新收到 Connected 事件
|
||||
动作:清空旧会话状态 → 重新执行首次同步
|
||||
目标:确保重连后数据与服务器完全一致
|
||||
|
||||
⑦ 同步失败处理
|
||||
触发:TSBridge.getChannelsJSON() / TSBridge.getClientsJSON() 请求失败
|
||||
动作:标记同步失败 → 不进入业务就绪 → 允许重试
|
||||
目标:防止在不完整数据上执行业务操作
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、文件索引
|
||||
|
||||
### 主流程
|
||||
|
||||
| 文件 | 用户流程 | 主要内容 |
|
||||
| --- | --- | --- |
|
||||
| [01_连接服务器.md](01_连接服务器.md) | 输入信息 → 建立连接 → 首次同步 | 生命周期、初始化配置、构造连接时序、状态树、事件依赖 |
|
||||
| [02_浏览频道.md](02_浏览频道.md) | 浏览频道列表 → 查看成员 → Poke | 同步状态、成员状态、成员事件时序、成员实体状态树 |
|
||||
| [03_切换频道.md](03_切换频道.md) | 选择频道 → 输入密码 → 等待确认 | 频道切换时序、切换状态机 |
|
||||
| [04_文本消息.md](04_文本消息.md) | 发送消息 → 接收消息 → 消息归档 | 文本消息时序、聊天状态树 |
|
||||
| [05_语音通信.md](05_语音通信.md) | PTT 按下 → 发送语音帧 → 松开 | 语音发送时序、语音状态树 |
|
||||
| [06_文件传输.md](06_文件传输.md) | 初始化 → TCP 连接 → 数据传输 | 文件传输时序、文件状态树 |
|
||||
| [07_断开连接.md](07_断开连接.md) | 主动断开 / 被动断开 / 被踢 | 断开时序、清理流程 |
|
||||
| [09_EventBus架构.md](09_EventBus架构.md) | TS 事件与渲染线程分离 | 事件定义、合并策略、线程模型 |
|
||||
|
||||
> **注**:流程文档中的 API 调用通过 `TSBridge` 单例,事件通过 `EventBus` 接收。
|
||||
> 底层通过 gomobile AAR 调用 Go SDK。详见 [sdk-bridge-api.md](../sdk-bridge-api.md) 和 [09_EventBus架构.md](09_EventBus架构.md)。
|
||||
|
||||
### 横切流程
|
||||
|
||||
| 文件 | 说明 | 触发时机 |
|
||||
| --- | --- | --- |
|
||||
| [08_状态同步.md](08_状态同步.md) | 独立于主流程之外的频道/成员/消息状态同步 | 连接建立、事件到达、数据不一致、重新连接时触发 |
|
||||
|
||||
> 所有流程文档中的 API 调用通过 `TSBridge` 单例,事件通过 `EventBus` 接收。
|
||||
> 底层通过 gomobile AAR 调用 Go SDK。
|
||||
|
||||
---
|
||||
|
||||
## 四、图例
|
||||
|
||||
本文将 SDK 行为分成三类:
|
||||
|
||||
| 类型 | 含义 | Kotlin API 示例 |
|
||||
| --- | --- | --- |
|
||||
| 本地调用 | 只在客户端进程内执行,不直接产生网络通信 | `TsClient.getClientId()`、`TsClient.getSelf()` |
|
||||
| 客户端请求 | 客户端主动向 TeamSpeak 服务器发送命令并等待响应 | `TsClient.connect()`、`TsClient.getChannelList()`、`TsClient.clientMove()` |
|
||||
| 服务端推送 | TeamSpeak 服务器主动下发通知,由 SDK 转换成 EventBus 事件 | `ClientEnter`、`ClientMoved`、`TextMessage` |
|
||||
|
||||
所有 Mermaid 图块均使用"技术名称 + 中文注释"的形式。箭头表示调用、数据流或前置依赖,不表示所有步骤都能互换顺序。
|
||||
|
||||
---
|
||||
|
||||
## 五、运行架构
|
||||
|
||||
### SDK 内部与上层应用的运行关系
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph APP[上层应用层<br/>负责 UI、业务状态和用户意图]
|
||||
UI[界面与交互<br/>发起连接、切换、聊天和 PTT]
|
||||
SM[业务状态机<br/>管理连接、同步、成员和语音状态]
|
||||
STORE[实体与消息存储<br/>保存频道、客户端和聊天事实]
|
||||
end
|
||||
|
||||
subgraph BRIDGE[桥接层<br/>TSBridge 单例<br/>负责 Kotlin 友好封装与生命周期管理]
|
||||
TSB[TSBridge<br/>connect、getChannelsJSON、moveToChannel 等 API]
|
||||
CB[JNI EventCallback<br/>Go 回调转 EventBus 事件]
|
||||
end
|
||||
|
||||
subgraph EB[事件总线层<br/>EventBus<br/>负责事件收集、合并与分发]
|
||||
BUS[EventBus<br/>SharedFlow 事件流]
|
||||
MERGE[事件合并器<br/>debounce 高频成员变化]
|
||||
THREAD[线程调度<br/>JNI 线程 → Main 线程]
|
||||
end
|
||||
|
||||
subgraph SDK[teamspeak-go SDK (gomobile AAR)<br/>负责协议、连接和事件转换]
|
||||
API[Go 公开方法<br/>Connect、GetChannelsJSON、SendVoice 等]
|
||||
CMDMW[命令中间件链<br/>拦截发送前的客户端请求]
|
||||
CMDBUS[命令通道<br/>构建、转义并发送协议命令]
|
||||
EVTBUS[事件分发器<br/>接收并解析 notify* 通知]
|
||||
EVTMW[事件中间件链<br/>拦截分发前的服务端事件]
|
||||
HANDLER[Go 回调 → gomobile 转换<br/>将 Go 结构体转为 Java 对象]
|
||||
VOICE[语音通道<br/>发送原始 Opus 语音帧]
|
||||
FILE[文件传输协调器<br/>初始化上传或下载会话]
|
||||
end
|
||||
|
||||
subgraph NET[网络与服务器层<br/>负责 TeamSpeak 实际通信]
|
||||
UDP[UDP 协议连接<br/>承载握手、命令、事件和语音]
|
||||
TCP[文件传输 TCP 连接<br/>承载上传和下载数据]
|
||||
SERVER[TeamSpeak 服务器<br/>提供频道、成员、消息和权限]
|
||||
end
|
||||
|
||||
UI --> SM
|
||||
SM --> TSB
|
||||
TSB --> API
|
||||
API --> CMDMW
|
||||
CMDMW --> CMDBUS
|
||||
CMDBUS --> UDP
|
||||
UDP <--> SERVER
|
||||
SERVER --> UDP
|
||||
UDP --> EVTBUS
|
||||
EVTBUS --> EVTMW
|
||||
EVTMW --> HANDLER
|
||||
HANDLER --> CB
|
||||
CB --> BUS
|
||||
BUS --> MERGE
|
||||
MERGE --> THREAD
|
||||
THREAD --> SM
|
||||
SM --> STORE
|
||||
API --> VOICE
|
||||
VOICE --> UDP
|
||||
API --> FILE
|
||||
FILE --> TCP
|
||||
TCP <--> SERVER
|
||||
```
|
||||
|
||||
### 三条核心运行通道
|
||||
|
||||
**命令通道**
|
||||
|
||||
```text
|
||||
TSBridge → gomobile → Go SDK API → CommandMiddleware → 协议命令 → UDP → 服务器响应
|
||||
```
|
||||
|
||||
适用:`TSBridge.getChannelsJSON()`、`TSBridge.getClientsJSON()`、`TSBridge.moveToChannel()`、`TSBridge.sendChannelMessage()`、`TSBridge.sendVoice()`。
|
||||
|
||||
**事件通道**
|
||||
|
||||
```text
|
||||
服务器 notify* → UDP → Go SDK 解析 → EventMiddleware → gomobile 回调 → TSBridge EventCallback → EventBus → ViewModel → StateFlow → UI
|
||||
```
|
||||
|
||||
适用:`Connected`、`Disconnected`、`ClientEnter`、`ClientLeave`、`ClientMoved`、`TextMessage`、`Poked`、`Kicked`。
|
||||
|
||||
**语音通道(绕过 EventBus)**
|
||||
|
||||
```text
|
||||
Go 语音包 → JNI OnVoiceData → VoiceService (Dispatchers.IO) → Opus 解码 → AudioTrack 播放
|
||||
```
|
||||
|
||||
语音数据延迟敏感,不经过 EventBus,由 VoiceService 在 IO 线程直接处理。
|
||||
|
||||
**文件传输通道**
|
||||
|
||||
```text
|
||||
初始化命令 → 获得 host/port/key → DialFileTransfer → TCP 数据传输
|
||||
```
|
||||
|
||||
文件内容不直接复用普通命令通道,初始化结果是建立 TCP 传输连接的前置条件。
|
||||
|
||||
### 中间件执行方向
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[上层业务命令<br/>通过 TSBridge 调用 API] --> B[CommandMiddleware 外层<br/>执行日志、鉴权或限流]
|
||||
B --> C[CommandMiddleware 内层<br/>修改或检查协议命令]
|
||||
C --> D[SDK 命令发送器<br/>编码并发送 TeamSpeak 命令]
|
||||
D --> E[TeamSpeak 服务器<br/>执行命令并返回响应]
|
||||
|
||||
E --> F[SDK 事件解析器<br/>解析 notify* 服务端通知]
|
||||
F --> G[EventMiddleware 内层<br/>检查或转换原始事件]
|
||||
G --> H[EventMiddleware 外层<br/>记录事件或统一过滤]
|
||||
H --> I[gomobile 回调 → TSBridge<br/>JNI 回调转 EventBus 事件]
|
||||
I --> J[EventBus<br/>合并/节流后分发给 ViewModel]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、统一实现原则
|
||||
|
||||
1. **先注册事件,再连接。** 避免握手完成后的首批推送无人处理。
|
||||
2. **先等待连接就绪,再发送业务命令。** `Connect` 不代替 `WaitConnected`。
|
||||
3. **列表命令建立基线,服务端事件维护增量。** 两者缺一不可。
|
||||
4. **成员表按 ClientID 幂等更新。** 不使用独立 `+1/-1` 长期维护频道人数。
|
||||
5. **命令响应和事件事实分离。** 尤其是频道移动,必须等待 `OnClientMoved`。
|
||||
6. **当前用户通过 ClientID 识别。** 不依赖昵称或 UI 当前页面判断自己。
|
||||
7. **未知实体引用触发补偿同步。** 不静默忽略移动或离开事件。
|
||||
8. **消息按 TargetMode 和 Target 归档。** 不使用当前页面频道代替真实目标。
|
||||
9. **语音只在连接和频道前置条件满足时发送。** 断开或被踢必须立即停止。
|
||||
10. **文件传输严格遵守"初始化 → TCP 连接 → 数据传输"。** 三个阶段不能跳过。
|
||||
11. **中间件必须正确调用下一层。** 吞掉命令会导致请求不发送,吞掉事件会导致状态缺失。
|
||||
12. **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
|
||||
13. **事件通过 EventBus 分发,不在 JNI 回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`,ViewModel 通过 `collect()` 响应。高频成员变化事件 debounce 合并,避免事件风暴。
|
||||
@@ -0,0 +1,244 @@
|
||||
# 连接服务器
|
||||
|
||||
> 用户流程:输入服务器地址 → 输入昵称 → 输入密码(可选) → 点击进入服务器 → 连接成功 → 首次同步
|
||||
> 对应程序流程:准备配置 → TSBridge.connect() → EventBus 收到 Connected 事件 → 首次同步
|
||||
|
||||
---
|
||||
|
||||
## 一、生命周期
|
||||
|
||||
### 1.1 完整生命周期
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[准备配置<br/>地址、昵称、密码] --> B[TSBridge.connect<br/>组装连接参数并注册 EventCallback]
|
||||
B --> C[Go SDK Connect<br/>通过 gomobile 发起 UDP 会话与协议握手]
|
||||
C --> D{连接请求是否成功<br/>判断 connect 返回值}
|
||||
D -- 否 --> E[连接失败处理<br/>记录错误并释放客户端资源]
|
||||
D -- 是 --> F{是否已收到 Connected 事件<br/>等待 EventBus 连接就绪事件}
|
||||
F -- 否 --> G[等待失败处理<br/>处理超时或握手异常]
|
||||
F -- 是 --> H[EventBus Connected 事件<br/>接收连接成功事件]
|
||||
H --> I[首次同步<br/>请求频道、成员和自身身份信息]
|
||||
I --> J[运行就绪<br/>允许发送命令、聊天和语音]
|
||||
J --> K[持续接收 EventBus 事件<br/>归并成员、移动、消息和 Poke]
|
||||
K --> J
|
||||
J --> L{结束原因<br/>区分主动断开与被动中断}
|
||||
L -- 用户主动退出 --> M[TSBridge.disconnect<br/>发送优雅关闭请求]
|
||||
L -- 网络或服务端中断 --> N[EventBus Disconnected 事件<br/>接收断开原因并停止写操作]
|
||||
L -- 自己被踢出 --> O[EventBus Kicked 事件<br/>接收频道踢或服务器踢原因]
|
||||
M --> P[清理运行资源<br/>停止语音、取消请求并清空会话状态]
|
||||
N --> P
|
||||
O --> P
|
||||
P --> Q[生命周期结束<br/>不再承担当前会话请求]
|
||||
```
|
||||
|
||||
### 生命周期约束
|
||||
|
||||
1. `TSBridge.connect()` 发起连接,连接就绪通过 EventBus `Connected` 事件确认。
|
||||
2. EventBus 事件监听应在 `connect()` 之前开始收集,避免连接早期事件无人接收。
|
||||
3. `connect()` 成功只表示连接流程已启动;发送普通命令前必须等待 `Connected` 事件。
|
||||
4. `Connected` 是 SDK 推送的已连接事实,可作为首次同步入口。
|
||||
5. `TSBridge.disconnect()` 用于主动优雅退出;`Disconnected` 事件用于观察最终断开结果或异常断开。
|
||||
6. `Kicked` 事件来源于 `notifyclientleftview` 的特定原因,必须与普通成员离开区分。
|
||||
7. 断开后应停止语音、拒绝新命令,并清理只属于当前会话的状态。
|
||||
|
||||
### 1.2 生命周期阶段与允许操作
|
||||
|
||||
| 阶段 | 允许操作 | 禁止或不建议操作 | 中文说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 构造前 | 准备配置 | 调用任何 TSBridge 方法 | 连接尚未建立 |
|
||||
| 已配置 | 注册 EventBus 事件收集、读取本地配置 | 发送服务器命令 | 尚未建立网络会话 |
|
||||
| 连接中 | 等待 `Connected` 事件、响应取消 | `getClientsJSON()`、`moveToChannel()` 等业务命令 | 握手尚未确认完成 |
|
||||
| 已连接 | 同步频道和成员、发送聊天、发送语音 | 重复调用 `connect()` | SDK 已具备业务通信条件 |
|
||||
| 断开中 | 停止上层写操作、等待清理 | 发起新业务命令 | 会话正在关闭 |
|
||||
| 已断开 | 释放资源或重新连接 | 复用失效会话发送命令 | 旧会话不再可信 |
|
||||
|
||||
---
|
||||
|
||||
## 二、初始化配置
|
||||
|
||||
### 2.1 初始化配置结构
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[应用启动配置<br/>收集身份、地址、昵称和可选参数] --> B[Identity<br/>提供 TeamSpeak 加密身份]
|
||||
A --> C[服务器地址 addr<br/>提供域名、IP 或 TSDNS 地址]
|
||||
A --> D[昵称 nickname<br/>提供当前连接显示名称]
|
||||
A --> E[连接选项<br/>提供可选运行能力]
|
||||
|
||||
E --> E3[serverPassword<br/>设置服务器连接密码]
|
||||
E --> E4[defaultChannel<br/>设置连接后默认进入的频道名称]
|
||||
E --> E5[defaultChannelPassword<br/>设置默认频道密码]
|
||||
|
||||
B --> F[TSBridge.connect<br/>组装参数并通过 gomobile 发起连接]
|
||||
C --> F
|
||||
D --> F
|
||||
E3 --> F
|
||||
E4 --> F
|
||||
E5 --> F
|
||||
|
||||
F --> G[注册 EventCallback<br/>JNI 回调转 EventBus 事件]
|
||||
G --> H[Go SDK Connect<br/>UDP 会话与协议握手]
|
||||
```
|
||||
|
||||
### 2.2 推荐初始化顺序
|
||||
|
||||
```kotlin
|
||||
// TSBridge.connect() 内部注册 JNI EventCallback,
|
||||
// 将所有 Go 回调转为 EventBus.emit() 事件。
|
||||
// ViewModel 通过 EventBus.events.collect() 接收事件并更新状态。
|
||||
|
||||
// 示例:ServerViewModel 中收集连接事件
|
||||
viewModelScope.launch {
|
||||
EventBus.events.collect { event ->
|
||||
when (event) {
|
||||
is TSEvent.Connected -> {
|
||||
// 开始首次同步
|
||||
performInitialSync()
|
||||
}
|
||||
is TSEvent.Disconnected -> {
|
||||
// 清理会话
|
||||
handleDisconnect(event.message)
|
||||
}
|
||||
else -> { /* 其他事件由对应 ViewModel 处理 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connect 发起网络连接,Connected 事件确认握手真正完成。
|
||||
val error = TSBridge.connect(host, nickname, password, defaultChannel, defaultChannelPassword, callbacks)
|
||||
// error 为空字符串表示连接流程已启动,等待 EventBus Connected 事件
|
||||
```
|
||||
|
||||
### 2.3 配置之间的依赖
|
||||
|
||||
| 配置 | 前置条件 | 影响阶段 | 依赖说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Identity` | 必须可用 | 连接、握手 | 缺少身份无法正确创建客户端 |
|
||||
| `addr` | 必须非空且可解析 | 地址解析、连接 | 由 SDK 内部解析器解析 |
|
||||
| `nickname` | 必须满足服务器命名规则 | 握手、上线 | 服务器可能拒绝无效或冲突昵称 |
|
||||
| `serverPassword` | 服务器启用密码时需要 | 握手 | 密码错误会导致连接失败 |
|
||||
| `defaultChannel` | 目标频道名称存在 | 连接完成阶段 | SDK 尝试在连接后自动进入该频道 |
|
||||
| `defaultChannelPassword` | 已设置默认频道且频道有密码 | 进入默认频道 | 单独设置密码而无默认频道没有明确目标 |
|
||||
|
||||
---
|
||||
|
||||
## 三、状态树
|
||||
|
||||
### Client 总状态树(连接相关部分)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ROOT[Client 生命周期状态树<br/>描述一个连接从创建到结束的完整状态]
|
||||
|
||||
ROOT --> U[未配置 Uninitialized<br/>仅准备 Identity 和连接参数]
|
||||
ROOT --> C[已配置 Configured<br/>EventBus 已注册但尚未连接]
|
||||
ROOT --> N[连接过程 Connecting<br/>正在解析地址并执行握手]
|
||||
ROOT --> R[已连接 Connected<br/>握手完成且可以执行业务命令]
|
||||
|
||||
U --> U1[配置 Identity<br/>准备加密身份]
|
||||
U --> U2[配置地址与昵称<br/>准备基础连接参数]
|
||||
U --> U3[配置连接选项<br/>准备密码和默认频道]
|
||||
|
||||
C --> C1[注册 EventBus 事件收集<br/>绑定服务端推送处理器]
|
||||
C --> C2[等待 connect 调用<br/>尚不能发送业务命令]
|
||||
|
||||
N --> N1[Resolving<br/>SDK 内部解析地址]
|
||||
N --> N2[Handshaking<br/>通过 UDP 建立协议会话]
|
||||
N --> N3[WaitingReady<br/>等待 Connected 事件]
|
||||
N --> N4[ConnectFailed<br/>解析、密码、网络或握手失败]
|
||||
|
||||
R --> R1[Syncing<br/>通过列表命令建立频道和成员基线]
|
||||
R --> R2[Ready<br/>允许聊天、移动、查询和语音]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、时序:构造、连接与首次同步
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 上层应用<br/>发起连接并展示状态
|
||||
participant VM as ViewModel<br/>收集 EventBus 事件
|
||||
participant BRIDGE as TSBridge<br/>管理连接生命周期
|
||||
participant SDK as Go SDK (gomobile)<br/>管理协议通信
|
||||
participant TS as TeamSpeak 服务器<br/>执行握手和列表请求
|
||||
participant STORE as 业务状态仓库<br/>保存频道和成员基线
|
||||
|
||||
UI->>BRIDGE: connect(host, nickname, password, ...)
|
||||
BRIDGE->>SDK: gomobile 调用 Go Connect
|
||||
SDK->>TS: 连接握手请求<br/>协商身份、密码和会话
|
||||
TS-->>SDK: 握手响应
|
||||
SDK-->>BRIDGE: connect 返回 ""
|
||||
|
||||
SDK->>BRIDGE: OnConnected 回调<br/>gomobile 转换为 JNI 回调
|
||||
BRIDGE->>BRIDGE: EventBus.emit(Connected)
|
||||
BRIDGE->>VM: EventBus SharedFlow<br/>分发 Connected 事件
|
||||
VM->>VM: 更新连接状态
|
||||
|
||||
par 请求频道基线
|
||||
VM->>BRIDGE: TSBridge.getChannelsJSON()
|
||||
BRIDGE->>SDK: gomobile 调用 GetChannelsJSON
|
||||
SDK->>TS: channellist
|
||||
TS-->>SDK: JSON 字符串
|
||||
SDK-->>BRIDGE: 频道 JSON
|
||||
BRIDGE-->>VM: JSON 字符串
|
||||
VM->>STORE: 解析并保存频道基线
|
||||
and 请求成员基线
|
||||
VM->>BRIDGE: TSBridge.getClientsJSON()
|
||||
BRIDGE->>SDK: gomobile 调用 GetClientsJSON
|
||||
SDK->>TS: clientlist
|
||||
TS-->>SDK: JSON 字符串
|
||||
SDK-->>BRIDGE: 成员 JSON
|
||||
BRIDGE-->>VM: JSON 字符串
|
||||
VM->>STORE: 解析并保存成员基线
|
||||
and 读取自身 ID
|
||||
VM->>BRIDGE: TSBridge.getClientID()
|
||||
BRIDGE-->>VM: selfClientID
|
||||
VM->>STORE: 保存自身 ID
|
||||
end
|
||||
|
||||
VM->>STORE: 原子提交频道、成员和自身 ID
|
||||
STORE-->>UI: 同步完成<br/>连接状态进入业务就绪
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、事件依赖
|
||||
|
||||
### 连接相关前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `TSBridge.connect` | 地址、昵称 | 已注册 EventBus 事件收集 | 参数无效时不进入连接流程 |
|
||||
| `connect` 成功 | 已注册 EventBus 事件收集 | — | 返回错误信息并停止等待连接 |
|
||||
| `Connected` 事件 | 已调用 `connect` | — | 超时后终止本次初始化 |
|
||||
| 首次同步 | `Connected` 事件 | 同时读取频道、成员和自身 ID | 任一核心请求失败则不标记业务就绪 |
|
||||
|
||||
### 事件依赖总图(连接阶段)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CONFIG[初始化配置完成<br/>地址、昵称已准备] --> REG[注册 EventBus 事件收集<br/>确保早期服务端推送可被处理]
|
||||
REG --> CONNECT[TSBridge.connect<br/>启动地址解析、UDP 会话和握手]
|
||||
CONNECT --> CONNECTED[EventBus Connected 事件<br/>SDK 推送连接成功事实]
|
||||
|
||||
CONNECTED --> CID[TSBridge.getClientID<br/>读取当前用户客户端 ID]
|
||||
CONNECTED --> CHANNELS[TSBridge.getChannelsJSON<br/>建立频道基线]
|
||||
CONNECTED --> CLIENTS[TSBridge.getClientsJSON<br/>建立在线成员基线]
|
||||
|
||||
CID --> SELFREADY[当前用户身份就绪<br/>能够识别自己的移动和离开事件]
|
||||
CHANNELS --> DATAREADY[服务器实体就绪<br/>频道和成员引用关系可被验证]
|
||||
CLIENTS --> DATAREADY
|
||||
SELFREADY --> READY[业务就绪<br/>允许移动、聊天和语音]
|
||||
DATAREADY --> READY
|
||||
```
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 是否完成连接 | EventBus `Connected` 事件 | `connect()` 只负责启动连接 |
|
||||
| 自身客户端 ID | `TSBridge.getClientID()` | SDK 本地缓存服务器分配的 ID |
|
||||
| 初始频道列表 | `TSBridge.getChannelsJSON()` 响应 | SDK 未列出频道创建、更新、删除事件 |
|
||||
| 初始在线成员 | `TSBridge.getClientsJSON()` 响应 | 建立完整在线成员基线 |
|
||||
@@ -0,0 +1,131 @@
|
||||
# 浏览频道与成员
|
||||
|
||||
> 用户流程:分配到默认频道 → 浏览频道列表 → 浏览频道中的人员分布 → poke 服务器内成员
|
||||
> 对应程序流程:TsClient.getChannelList() 建立频道基线 → TsClient.getClientList() 建立成员基线 → 持续接收 onClient* 事件增量更新
|
||||
|
||||
---
|
||||
|
||||
## 一、状态树
|
||||
|
||||
### 同步状态
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SYNC[同步状态<br/>建立服务器数据基线]
|
||||
|
||||
SYNC --> S0[未同步 Unsynced<br/>已连接但尚无完整数据]
|
||||
SYNC --> S1[同步中 Syncing<br/>调用 getChannelList 和 getClientList]
|
||||
SYNC --> S2[已同步 Synchronized<br/>列表基线可供 UI 使用]
|
||||
SYNC --> S3[同步失败 SyncFailed<br/>列表请求失败或上下文失效]
|
||||
```
|
||||
|
||||
### 成员状态
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
MEMBER[成员状态<br/>维护在线用户及频道位置]
|
||||
|
||||
MEMBER --> M0[空基线 Empty<br/>尚未执行 ListClients]
|
||||
MEMBER --> M1[已有基线 Loaded<br/>getClientList 已建立成员表]
|
||||
MEMBER --> M2[增量更新 Updating<br/>OnClientEnter/Leave/Moved 正在归并]
|
||||
MEMBER --> M3[不一致 Inconsistent<br/>事件引用未知成员或频道]
|
||||
```
|
||||
|
||||
### 成员实体状态树
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ENTITY[成员实体状态树<br/>描述 ListClients 基线与实时事件如何协同]
|
||||
|
||||
ENTITY --> BASE[基线分支<br/>由客户端请求建立完整成员集合]
|
||||
ENTITY --> DELTA[增量分支<br/>由服务端推送维护实时变化]
|
||||
ENTITY --> SELF[当前用户分支<br/>通过 ClientID 识别自己的事件]
|
||||
ENTITY --> REPAIR[修复分支<br/>处理事件缺失或引用不一致]
|
||||
|
||||
BASE --> B1[getClientList 请求<br/>获取服务器当前全部在线客户端]
|
||||
B1 --> B2[按 clientID 建表<br/>使用客户端 ID 去重保存]
|
||||
B2 --> B3[按 channelID 建索引<br/>派生每个频道的成员列表]
|
||||
|
||||
DELTA --> D1[onClientEnter<br/>新增或覆盖进入视野的成员]
|
||||
DELTA --> D2[onClientMoved<br/>覆盖成员的 channelID]
|
||||
DELTA --> D3[onClientLeave<br/>按 clientID 幂等删除成员]
|
||||
|
||||
SELF --> S1[getClientId 本地调用<br/>读取服务器分配的自身 ID]
|
||||
S1 --> S2[事件 clientID 比对<br/>判断移动或离开事件是否属于自己]
|
||||
S2 --> S3[更新自身频道事实<br/>自己的移动决定实际所在频道]
|
||||
|
||||
REPAIR --> R1[检测未知 clientID<br/>移动或离开事件找不到实体]
|
||||
R1 --> R2[重新调用 getClientList<br/>用完整列表修复成员基线]
|
||||
REPAIR --> R3[检测未知 channelID<br/>成员引用本地不存在的频道]
|
||||
R3 --> R4[重新调用 getChannelList<br/>用完整列表修复频道基线]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、时序:成员进入、移动与离开
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TS as TeamSpeak 服务器<br/>产生在线成员变化
|
||||
participant SDK as Go SDK (gomobile)<br/>解析 notify* 通知
|
||||
participant BRIDGE as TSBridge<br/>JNI 回调转 EventBus
|
||||
participant EB as EventBus<br/>事件合并与分发
|
||||
participant REDUCER as ViewModel<br/>debounce 后刷新成员
|
||||
participant STORE as 成员实体仓库<br/>以 clientID 保存唯一成员
|
||||
participant UI as 频道树 UI<br/>派生人数和成员预览
|
||||
|
||||
TS-->>SDK: notifycliententerview
|
||||
SDK->>BRIDGE: OnClientEnter(Client)
|
||||
BRIDGE->>EB: emit(ClientEnter)
|
||||
|
||||
TS-->>SDK: notifyclientmoved
|
||||
SDK->>BRIDGE: OnClientMoved(id, channelID)
|
||||
BRIDGE->>EB: emit(ClientMoved)
|
||||
|
||||
TS-->>SDK: notifyclientleftview
|
||||
SDK->>BRIDGE: OnClientLeave(id, reason)
|
||||
BRIDGE->>EB: emit(ClientLeave)
|
||||
|
||||
Note over EB: debounce 300ms 合并
|
||||
|
||||
EB->>REDUCER: collect → refreshClientList()
|
||||
REDUCER->>STORE: TSBridge.getClientsJSON() → 全量刷新
|
||||
STORE-->>UI: 实体引用变化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、事件依赖
|
||||
|
||||
### 成员相关前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `getClientsJSON` 增量归并 | 已有频道基线更易校验 | 与 `getChannelsJSON` 同一同步批次 | 未知频道触发频道补偿读取 |
|
||||
| `ClientEnter` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
|
||||
| `ClientMoved` 事件 | 已注册 EventBus 事件收集 | 已有成员基线和自身 ID | debounce 后全量刷新 |
|
||||
| `ClientLeave` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
|
||||
|
||||
### 服务端通知映射
|
||||
|
||||
| 服务端通知 | TSBridge 回调 → EventBus 事件 | 上层状态依赖 | 中文说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `notifycliententerview` | `OnClientEnter` → `ClientEnter` | 成员实体表、频道成员 Selector | 新成员进入视野后 debounce 合并刷新 |
|
||||
| `notifyclientleftview` | `OnClientLeave` → `ClientLeave` | 成员实体表、离开原因 | debounce 合并刷新 |
|
||||
| `notifyclientmoved` | `OnClientMoved` → `ClientMoved` | 成员位置、当前用户频道 | debounce 合并刷新 |
|
||||
| `notifyclientpoke` | `OnPoked` → `Poked` | Poke 通知或成员卡 | 立即分发,展示发送者和 Poke 内容 |
|
||||
|
||||
### 缺失事件能力带来的依赖限制
|
||||
|
||||
根据 SDK 文档,当前公开事件不包含频道创建、频道更新和频道删除。因此:
|
||||
|
||||
1. `TsClient.getChannelList()` 是频道目录的主要权威来源。
|
||||
2. 不能假设频道列表会依靠 `onClient*` 事件永久保持最新。
|
||||
3. 发现成员引用未知频道时,应重新调用 `TsClient.getChannelList()`。
|
||||
4. 如果产品需要实时频道管理,应扩展 SDK 对相应 `notify*` 通知的支持。
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 成员实时变化 | EventBus `ClientEnter/Leave/Moved` 事件 | 服务端主动推送实时事实,debounce 后全量刷新 |
|
||||
@@ -0,0 +1,84 @@
|
||||
# 切换频道
|
||||
|
||||
> 用户流程:选择目标频道 → (频道有密码时输入密码) → 发送切换请求 → 等待服务端确认 → 进入目标频道
|
||||
> 对应程序流程:TsClient.clientMove(selfID, targetID, password) → 等待 onClientMoved 服务端事实 → 比对 clientID → 提交新频道
|
||||
|
||||
---
|
||||
|
||||
## 一、时序:当前用户切换频道
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 频道选择 UI<br/>表达用户切换意图
|
||||
participant FSM as 切换状态机<br/>管理确认、请求和等待事实
|
||||
participant BRIDGE as TSBridge<br/>调用 TsClient API
|
||||
participant SDK as Go SDK (gomobile)<br/>发送 ClientMove 命令
|
||||
participant TS as TeamSpeak 服务器<br/>验证权限、密码并执行移动
|
||||
participant EVT as EventBus<br/>处理 ClientMoved 服务端事实
|
||||
participant STORE as 当前用户状态<br/>保存已确认频道
|
||||
|
||||
UI->>FSM: ChannelJoinRequested(targetID)
|
||||
FSM->>STORE: 读取 confirmedChannelID
|
||||
FSM-->>UI: 显示确认或密码输入
|
||||
UI->>FSM: ChannelJoinConfirmed(password)
|
||||
FSM->>FSM: 状态改为 requesting
|
||||
FSM->>BRIDGE: clientMove(selfID, targetID, password)
|
||||
BRIDGE->>SDK: gomobile ClientMove
|
||||
SDK->>TS: clientmove
|
||||
|
||||
alt 命令被服务器拒绝
|
||||
TS-->>SDK: error
|
||||
SDK-->>BRIDGE: 命令失败
|
||||
BRIDGE-->>FSM: 返回错误
|
||||
FSM->>FSM: 状态改为 failed
|
||||
FSM-->>UI: 显示失败原因
|
||||
else 命令请求成功
|
||||
TS-->>SDK: command ok
|
||||
SDK-->>BRIDGE: 命令成功
|
||||
BRIDGE-->>FSM: 返回成功(不代表状态已提交)
|
||||
FSM->>FSM: 状态改为 waitingServerEvent
|
||||
TS-->>SDK: notifyclientmoved
|
||||
SDK->>EVT: onClientMoved(TsClientMoved)
|
||||
EVT->>STORE: 比对 clientID == selfID
|
||||
EVT->>STORE: 提交 channelID = targetID
|
||||
STORE-->>FSM: SelfMoved(targetID)
|
||||
FSM->>FSM: 状态恢复 idle
|
||||
FSM-->>UI: 导航到目标房间
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、事件依赖
|
||||
|
||||
### 频道切换前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `TsClient.clientMove` | 已连接、目标频道 ID、目标客户端 ID | 已同步频道和成员 | 命令失败保留原频道事实 |
|
||||
| 提交自己新频道 | 自己的 `onClientMoved` | 匹配目标频道和请求上下文 | 命令响应不能直接提交频道 |
|
||||
|
||||
### 命令响应与事件事实的区别
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
INTENT[用户意图<br/>例如发送消息或切换频道] --> COMMAND[客户端请求<br/>调用 SDK 命令方法]
|
||||
COMMAND --> RESPONSE{命令响应<br/>判断服务器是否接受请求}
|
||||
RESPONSE -- 失败 --> FAILED[请求失败<br/>保留原事实并展示原因]
|
||||
RESPONSE -- 成功 --> WAITFACT[等待事实事件<br/>仅对存在对应推送的操作适用]
|
||||
WAITFACT --> EVENT[服务端推送事件<br/>例如 OnClientMoved 或 OnTextMessage]
|
||||
EVENT --> COMMIT[提交服务器事实<br/>更新频道位置或消息实体]
|
||||
|
||||
RESPONSE -- 无对应事实事件 --> LOCALDONE[完成本次调用<br/>仅记录调用结果而不猜测远端效果]
|
||||
```
|
||||
|
||||
**关键区分:**
|
||||
|
||||
- `TsClient.clientMove` 返回成功:命令执行成功;当前用户频道应由自己的 `onClientMoved` 最终确认。
|
||||
- 命令成功 ≠ 状态已提交。必须等待服务端推送的 `onClientMoved` 事件才能更新本地频道事实。
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 当前用户频道 | 自己的 `onClientMoved` 事件 | 命令响应只表示服务器接受请求,不表示状态已生效 |
|
||||
@@ -0,0 +1,99 @@
|
||||
# 文本消息
|
||||
|
||||
> 用户流程:收到文本消息 → 发送文本消息 → 消息按目标归档
|
||||
> 对应程序流程:TsClient.sendTextMessage(targetMode, targetID, text) → 等待命令响应 → onTextMessage 按 targetMode/target 归档
|
||||
|
||||
---
|
||||
|
||||
## 一、状态树
|
||||
|
||||
### 聊天状态
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CHAT[聊天状态<br/>维护发送与接收消息]
|
||||
|
||||
CHAT --> T0[空闲 Idle<br/>没有正在发送的文本消息]
|
||||
CHAT --> T1[发送中 Sending<br/>sendTextMessage 等待响应]
|
||||
CHAT --> T2[发送失败 Failed<br/>命令返回错误]
|
||||
CHAT --> T3[收到消息 Received<br/>OnTextMessage 归档服务端推送]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、时序:发送与接收文本消息
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 聊天 UI<br/>输入消息并展示会话
|
||||
participant CHAT as 聊天状态机<br/>管理发送 Pending 和失败
|
||||
participant BRIDGE as TSBridge<br/>调用 TsClient API
|
||||
participant SDK as Go SDK (gomobile)<br/>发送并解析文本消息
|
||||
participant TS as TeamSpeak 服务器<br/>路由私聊、频道或服务器消息
|
||||
participant STORE as 消息仓库<br/>按真实目标会话归档
|
||||
|
||||
UI->>CHAT: MessageSendRequested(targetMode, targetID, text)
|
||||
CHAT->>CHAT: 创建 requestId 与 sending 状态
|
||||
CHAT->>BRIDGE: sendTextMessage(targetMode, targetID, text)
|
||||
BRIDGE->>SDK: gomobile SendTextMessage
|
||||
SDK->>TS: sendtextmessage
|
||||
|
||||
alt 发送失败
|
||||
TS-->>SDK: error
|
||||
SDK-->>BRIDGE: 命令失败
|
||||
BRIDGE-->>CHAT: 返回错误
|
||||
CHAT-->>UI: 标记消息发送失败
|
||||
else 发送成功
|
||||
TS-->>SDK: command ok
|
||||
SDK-->>BRIDGE: 命令成功
|
||||
BRIDGE-->>CHAT: 返回成功
|
||||
CHAT-->>UI: 标记发送完成
|
||||
end
|
||||
|
||||
TS-->>SDK: notifytextmessage
|
||||
SDK->>CHAT: onTextMessage(TsTextMessage)
|
||||
CHAT->>STORE: 按 targetMode 与 target 归档
|
||||
STORE-->>UI: 更新目标会话和未读状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、事件依赖
|
||||
|
||||
### 文本消息前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `TsClient.sendTextMessage` | 已连接、合法 targetMode 和 target | 已建立目标会话上下文 | 失败保留可重试消息状态 |
|
||||
| `onTextMessage` 归档 | `targetMode`、`target`、发送者信息 | 已有频道或私聊实体 | 不得使用当前页面猜测目标 |
|
||||
|
||||
### 服务端通知映射
|
||||
|
||||
| 服务端通知 | TSBridge 回调 → EventBus 事件 | 上层状态依赖 | 中文说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `notifytextmessage` | `OnTextMessage` → `TextMessage` | 消息归档、未读 | 立即分发,必须读取 targetMode 和 target |
|
||||
|
||||
### 命令响应与事件事实的区别
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
INTENT[用户意图<br/>发送文本消息] --> COMMAND[客户端请求<br/>SendTextMessage]
|
||||
COMMAND --> RESPONSE{命令响应<br/>判断服务器是否接受请求}
|
||||
RESPONSE -- 失败 --> FAILED[请求失败<br/>保留原事实并展示原因]
|
||||
RESPONSE -- 成功 --> WAITFACT[等待事实事件<br/>适用于有对应推送的操作]
|
||||
WAITFACT --> EVENT[服务端推送事件<br/>OnTextMessage]
|
||||
EVENT --> COMMIT[提交服务器事实<br/>归档消息实体]
|
||||
|
||||
RESPONSE -- 无对应事实事件 --> LOCALDONE[完成本次调用<br/>仅记录调用结果]
|
||||
```
|
||||
|
||||
**关键区分:**
|
||||
|
||||
- `TsClient.sendTextMessage` 返回成功:发送命令成功;如果服务器会向自己回推消息,可再用 `onTextMessage` 归档权威消息。
|
||||
- 消息归档必须依据 `targetMode` 和 `target`,不能使用当前页面频道代替真实目标。
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 文本消息 | `onTextMessage` | 来源是 `notifytextmessage` |
|
||||
@@ -0,0 +1,81 @@
|
||||
# 语音通信
|
||||
|
||||
> 用户流程:按下 PTT 按钮 → 发送语音 → 松开 PTT 按钮 → 停止语音
|
||||
> 对应程序流程:前置条件检查 → StartCapture → 循环 TsClient.sendVoice(clientID, codec, frame) → StopCapture
|
||||
|
||||
---
|
||||
|
||||
## 一、状态树
|
||||
|
||||
### 语音状态
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
VOICE[语音状态<br/>维护 Opus 帧发送过程]
|
||||
|
||||
VOICE --> V0[静默 Silent<br/>没有发送语音帧]
|
||||
VOICE --> V1[采集中 Capturing<br/>上层正在生成 Opus 帧]
|
||||
VOICE --> V2[发送中 Sending<br/>循环调用 sendVoice]
|
||||
VOICE --> V3[受阻 Blocked<br/>未连接、采集失败或发送异常]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、时序:发送语音帧与异常停止
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as PTT 控件<br/>接收按下、松开和取消操作
|
||||
participant VFSM as 语音发送状态机<br/>检查连接与频道前置条件
|
||||
participant CAP as 音频采集与编码器<br/>生成原始 Opus 帧
|
||||
participant BRIDGE as TSBridge<br/>调用 TsClient API
|
||||
participant SDK as Go SDK (gomobile)<br/>通过 SendVoice 发送语音
|
||||
participant TS as TeamSpeak 服务器<br/>转发语音给频道成员
|
||||
|
||||
UI->>VFSM: PttPressed
|
||||
VFSM->>VFSM: 检查 connected 和 joinedChannel
|
||||
|
||||
alt 前置条件不满足
|
||||
VFSM-->>UI: Blocked(reason)
|
||||
else 前置条件满足
|
||||
VFSM->>CAP: StartCapture()
|
||||
CAP-->>VFSM: CaptureReady
|
||||
VFSM->>VFSM: 状态改为 transmitting
|
||||
loop 每个 Opus 帧
|
||||
CAP-->>VFSM: opusFrame
|
||||
VFSM->>BRIDGE: sendVoice(clientID, codec, frame)
|
||||
BRIDGE->>SDK: gomobile SendVoice
|
||||
SDK->>TS: UDP voice frame
|
||||
end
|
||||
|
||||
alt 用户松开或取消
|
||||
UI->>VFSM: PttReleased / PointerCancelled
|
||||
VFSM->>CAP: StopCapture()
|
||||
VFSM->>VFSM: 状态恢复 silent
|
||||
else 连接断开
|
||||
SDK-->>VFSM: onDisconnected(error)
|
||||
VFSM->>CAP: StopCapture()
|
||||
VFSM->>VFSM: 状态改为 blocked
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、事件依赖
|
||||
|
||||
### 语音前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `TsClient.sendVoice` | 已连接、已有可发送的 Opus 帧 | 当前用户已在有效频道 | 任一前置失效立即停止发送 |
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 语音发送结果 | `TsClient.sendVoice` 返回值 | 表示本次本地发送调用结果,不代表对方播放成功 |
|
||||
|
||||
### 统一实现原则(语音相关)
|
||||
|
||||
- **语音只在连接和频道前置条件满足时发送。** 断开或被踢必须立即停止。
|
||||
@@ -0,0 +1,108 @@
|
||||
# 文件传输
|
||||
|
||||
> 用户流程:选择文件 → 上传/下载 → 等待传输完成
|
||||
> 对应程序流程:FileTransferInit → DialFileTransfer → UploadFileData / DownloadFileData
|
||||
> 注:文件传输 API 暂未在 `TsClient` 中封装,需通过 `TSBridge` 直接调用 gomobile 对象
|
||||
|
||||
---
|
||||
|
||||
## 一、状态树
|
||||
|
||||
### 文件状态
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
FILE[文件状态<br/>维护上传和下载过程]
|
||||
|
||||
FILE --> F0[空闲 Idle<br/>没有文件传输任务]
|
||||
FILE --> F1[初始化 Initializing<br/>请求 FileTransferInitUpload 或 Download]
|
||||
FILE --> F2[连接中 Dialing<br/>DialFileTransfer 建立 TCP 连接]
|
||||
FILE --> F3[传输中 Transferring<br/>UploadFileData 或 DownloadFileData 搬运数据]
|
||||
FILE --> F4[完成 Completed<br/>文件数据传输结束]
|
||||
FILE --> F5[失败 Failed<br/>初始化、连接或传输发生错误]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、时序:文件上传与下载
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 文件操作 UI<br/>选择上传或下载任务
|
||||
participant SDK as teamspeak-go SDK<br/>初始化文件传输并提供辅助函数
|
||||
participant TS as TeamSpeak 服务器<br/>分配文件传输连接信息
|
||||
participant TCP as 文件 TCP 连接<br/>承载实际文件字节流
|
||||
participant IO as 本地 Reader/Writer<br/>提供上传源或接收下载结果
|
||||
|
||||
alt 上传文件
|
||||
UI->>SDK: FileTransferInitUpload(channelID, path, password, size, overwrite)<br/>中文注释:请求初始化频道文件上传
|
||||
SDK->>TS: ftinitupload<br/>中文注释:提交目标路径、大小和覆盖选项
|
||||
TS-->>SDK: FileUploadInfo<br/>中文注释:返回 host、port 和传输 key
|
||||
SDK-->>UI: uploadInfo<br/>中文注释:初始化成功后才能建立数据连接
|
||||
UI->>SDK: DialFileTransfer(host, port, key)<br/>中文注释:建立独立 TCP 文件连接
|
||||
SDK->>TCP: TCP connect<br/>中文注释:连接服务器文件传输端口
|
||||
UI->>SDK: UploadFileData(host, info, reader)<br/>中文注释:从本地 Reader 读取上传内容
|
||||
IO-->>SDK: 文件字节流<br/>中文注释:持续提供待上传数据
|
||||
SDK->>TCP: 上传字节流<br/>中文注释:通过 TCP 发送文件内容
|
||||
TCP->>TS: 完成上传<br/>中文注释:服务器保存频道文件
|
||||
else 下载文件
|
||||
UI->>SDK: FileTransferInitDownload(channelID, path, password)<br/>中文注释:请求初始化频道文件下载
|
||||
SDK->>TS: ftinitdownload<br/>中文注释:提交目标频道和文件路径
|
||||
TS-->>SDK: FileDownloadInfo<br/>中文注释:返回 host、port 和传输 key
|
||||
SDK-->>UI: downloadInfo<br/>中文注释:初始化成功后才能建立数据连接
|
||||
UI->>SDK: DialFileTransfer(host, port, key)<br/>中文注释:建立独立 TCP 文件连接
|
||||
SDK->>TCP: TCP connect<br/>中文注释:连接服务器文件传输端口
|
||||
UI->>SDK: DownloadFileData(host, info, writer)<br/>中文注释:指定本地 Writer 接收文件
|
||||
TS->>TCP: 下载字节流<br/>中文注释:服务器持续发送文件内容
|
||||
TCP->>SDK: 文件字节流<br/>中文注释:SDK 从 tcp 读取下载数据
|
||||
SDK->>IO: 写入本地 Writer<br/>中文注释:保存或消费下载内容
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、事件依赖
|
||||
|
||||
### 文件传输前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `DialFileTransfer` | 上传或下载初始化结果 | 初始化信息尚未过期 | 不得跳过初始化直接拨号 |
|
||||
| `UploadFileData` | 上传信息、Reader、有效 TCP 条件 | 文件大小与声明一致 | 中止任务并展示传输错误 |
|
||||
| `DownloadFileData` | 下载信息、Writer、有效 TCP 条件 | 本地空间和写权限可用 | 中止任务并清理不完整结果 |
|
||||
|
||||
### 事件依赖图(文件传输)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
READY --> FTINIT[文件传输初始化<br/>请求上传或下载连接信息]
|
||||
FTINIT --> FTDIAL[DialFileTransfer<br/>依赖 host、port 和 key 建立 TCP]
|
||||
FTDIAL --> FTDATA[上传或下载数据<br/>依赖已初始化的文件连接]
|
||||
```
|
||||
|
||||
### 命令响应与事件事实的区别
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
INTENT[用户意图<br/>上传或下载文件] --> COMMAND[客户端请求<br/>FileTransferInit]
|
||||
COMMAND --> RESPONSE{命令响应<br/>判断服务器是否接受请求}
|
||||
RESPONSE -- 失败 --> FAILED[请求失败<br/>展示错误原因]
|
||||
RESPONSE -- 成功 --> INFO[获得传输参数<br/>host、port、key]
|
||||
INFO --> DIAL[建立 TCP 连接<br/>DialFileTransfer]
|
||||
DIAL --> TRANSFER[数据传输<br/>Upload/DownloadFileData]
|
||||
```
|
||||
|
||||
**关键区分:**
|
||||
|
||||
- `FileTransferInitUpload/Download` 成功:只获得传输参数;文件内容仍依赖后续 TCP 连接和数据传输。
|
||||
- 文件传输严格遵守"初始化 → TCP 连接 → 数据传输"三个阶段,不能跳过。
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 文件传输进度 | SDK 内部文件状态事件 | `notifystartupload`、`notifystartdownload`、`notifystatusfiletransfer` |
|
||||
|
||||
### 统一实现原则(文件传输相关)
|
||||
|
||||
- **文件传输严格遵守"初始化 → TCP 连接 → 数据传输"。** 三个阶段不能跳过。
|
||||
@@ -0,0 +1,108 @@
|
||||
# 断开连接
|
||||
|
||||
> 用户流程:主动断开 / 网络异常断开 / 被踢出 → 清理会话 → 返回主页
|
||||
> 对应程序流程:TsClient.disconnect() 或 onDisconnected(error) 或 onKicked(reason) → 停止写操作 → 清理会话状态
|
||||
|
||||
---
|
||||
|
||||
## 一、状态树
|
||||
|
||||
### Client 总状态树(断开相关部分)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ROOT[Client 生命周期状态树<br/>断开相关部分]
|
||||
|
||||
ROOT --> D[断开过程 Disconnecting<br/>正在主动或被动结束会话]
|
||||
ROOT --> X[已结束 Terminated<br/>当前会话资源已清理]
|
||||
|
||||
D --> D1[GracefulDisconnect<br/>用户调用 TsClient.disconnect 优雅退出]
|
||||
D --> D2[UnexpectedDisconnect<br/>网络或服务端导致 onDisconnected]
|
||||
D --> D3[Kicked<br/>onKicked 表示自己被踢]
|
||||
|
||||
X --> X1[清理命令请求<br/>取消未完成的业务操作]
|
||||
X --> X2[清理语音与传输<br/>停止语音和文件连接]
|
||||
X --> X3[清理会话状态<br/>移除频道成员等临时事实]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、时序:主动断开、异常断开与被踢
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 上层应用<br/>展示连接状态并触发退出
|
||||
participant VM as ViewModel<br/>收集 EventBus 事件
|
||||
participant BRIDGE as TSBridge<br/>管理连接生命周期
|
||||
participant SDK as Go SDK (gomobile)<br/>管理连接与断开事件
|
||||
participant TS as TeamSpeak 服务器<br/>关闭或终止客户端会话
|
||||
participant STORE as 会话数据仓库<br/>保存当前连接的临时事实
|
||||
|
||||
alt 用户主动断开
|
||||
UI->>VM: DisconnectRequested
|
||||
VM->>VM: 状态改为 disconnecting
|
||||
VM->>BRIDGE: TSBridge.disconnect()
|
||||
BRIDGE->>SDK: gomobile Disconnect
|
||||
SDK->>TS: shutdown reason
|
||||
TS-->>SDK: 连接关闭
|
||||
SDK-->>BRIDGE: OnDisconnected 回调
|
||||
BRIDGE->>BRIDGE: EventBus.emit(Disconnected)
|
||||
BRIDGE->>VM: EventBus 分发 Disconnected 事件
|
||||
else 网络或服务器异常
|
||||
TS--xSDK: UDP 会话中断
|
||||
SDK-->>BRIDGE: OnDisconnected 回调
|
||||
BRIDGE->>BRIDGE: EventBus.emit(Disconnected)
|
||||
BRIDGE->>VM: EventBus 分发 Disconnected 事件
|
||||
else 自己被踢
|
||||
TS-->>SDK: notifyclientleftview reasonid=4/5
|
||||
SDK-->>BRIDGE: OnKicked 回调
|
||||
BRIDGE->>BRIDGE: EventBus.emit(Kicked)
|
||||
BRIDGE->>VM: EventBus 分发 Kicked 事件
|
||||
end
|
||||
|
||||
VM->>VM: 停止语音与文件传输
|
||||
VM->>STORE: 清理频道、成员和 Pending
|
||||
STORE-->>UI: 返回已断开或失败页面
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、事件依赖
|
||||
|
||||
### 断开相关前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Disconnected` 事件 | 已注册 EventBus 事件收集 | 保存断开原因 | 停止全部依赖连接的操作 |
|
||||
| `Kicked` 事件 | 已注册 EventBus 事件收集 | 区分 reasonid 4 和 5 | 清理会话且不伪装为普通成员离开 |
|
||||
|
||||
### 事件依赖图(断开阶段)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
DISCONNECTED[EventBus Disconnected 事件<br/>接收连接终止事件] --> STOP[停止依赖操作<br/>禁止命令、语音和新文件传输]
|
||||
KICKED[EventBus Kicked 事件<br/>接收自己被踢事件] --> STOP
|
||||
STOP --> CLEAN[清理会话状态<br/>移除当前连接的实体与 Pending]
|
||||
```
|
||||
|
||||
### 服务端通知映射
|
||||
|
||||
| 服务端通知 | TSBridge 回调 → EventBus 事件 | 上层状态依赖 | 中文说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `notifyclientleftview`,`reasonid=4/5` 且为自己 | `OnKicked` → `Kicked` | 当前会话、语音、导航 | SDK 将自己被踢转换为独立事件 |
|
||||
|
||||
### 生命周期约束(断开相关)
|
||||
|
||||
- `TSBridge.disconnect()` 用于主动优雅退出;`Disconnected` 事件用于观察最终断开结果或异常断开。
|
||||
- `Kicked` 事件来源于 `notifyclientleftview` 的特定原因,必须与普通成员离开区分。
|
||||
- 断开后应停止语音、拒绝新命令,并清理只属于当前会话的状态。
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 被踢状态 | EventBus `Kicked` 事件 | SDK 已从离开事件原因中识别自己被踢 |
|
||||
|
||||
### 统一实现原则(断开相关)
|
||||
|
||||
- **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
|
||||
@@ -0,0 +1,318 @@
|
||||
# 状态同步
|
||||
|
||||
> 独立于主流程之外的横切关注点:每当连接建立、事件到达、数据不一致或重新连接时,都需要更新频道、成员和服务器信息。
|
||||
> 本流程不直接对应某个用户操作,而是被所有用户流程在特定步骤中触发。
|
||||
> 所有 API 调用通过 `TSBridge`,事件通过 `EventBus` 接收。
|
||||
|
||||
---
|
||||
|
||||
## 一、触发时机总览
|
||||
|
||||
```
|
||||
触发状态同步的时机
|
||||
│
|
||||
├─ ① 首次同步(连接建立后)
|
||||
│ 触发源:EventBus Connected 事件
|
||||
│ 动作:TSBridge.getChannelsJSON() + TSBridge.getClientsJSON() + TSBridge.getClientID()
|
||||
│ 目标:建立完整的频道基线和成员基线
|
||||
│
|
||||
├─ ② 增量同步(事件驱动)
|
||||
│ 触发源:EventBus ClientEnter / ClientMoved / ClientLeave 事件
|
||||
│ 动作:debounce 合并后 refreshClientList() 全量刷新
|
||||
│ 目标:维护实时成员位置和频道人数
|
||||
│
|
||||
├─ ③ 补偿同步(数据不一致时)
|
||||
│ 触发源:事件引用了未知的 clientID 或 channelID
|
||||
│ 动作:重新调用 TSBridge.getClientsJSON() 或 TSBridge.getChannelsJSON()
|
||||
│ 目标:用完整列表修复丢失的基线数据
|
||||
│
|
||||
├─ ④ 自身状态同步(频道切换后)
|
||||
│ 触发源:EventBus 收到自己的 ClientMoved 事件
|
||||
│ 动作:比对 clientID == selfID → 更新自身所在频道
|
||||
│ 目标:确认当前用户的实际频道位置
|
||||
│
|
||||
├─ ⑤ 消息归档同步(收到消息时)
|
||||
│ 触发源:EventBus TextMessage 事件
|
||||
│ 动作:按 targetMode + target 归档到正确的会话
|
||||
│ 目标:维护消息与目标频道的对应关系
|
||||
│
|
||||
├─ ⑥ 重连后全量同步(断开重连后)
|
||||
│ 触发源:重新收到 EventBus Connected 事件
|
||||
│ 动作:清空旧会话状态 → 重新执行首次同步
|
||||
│ 目标:确保重连后数据与服务器完全一致
|
||||
│
|
||||
└─ ⑦ 同步失败处理
|
||||
触发源:TSBridge.getChannelsJSON() / TSBridge.getClientsJSON() 请求失败
|
||||
动作:标记同步失败 → 不进入业务就绪 → 允许重试
|
||||
目标:防止在不完整数据上执行业务操作
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、同步状态树
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
SYNC[同步状态<br/>建立服务器数据基线]
|
||||
|
||||
SYNC --> S0[未同步 Unsynced<br/>已连接但尚无完整数据]
|
||||
SYNC --> S1[同步中 Syncing<br/>调用 getChannelList 和 getClientList]
|
||||
SYNC --> S2[已同步 Synchronized<br/>列表基线可供 UI 使用]
|
||||
SYNC --> S3[同步失败 SyncFailed<br/>列表请求失败或上下文失效]
|
||||
|
||||
S0 --> S1
|
||||
S1 --> S2
|
||||
S1 --> S3
|
||||
S3 --> S1
|
||||
S2 --> S1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、成员状态树
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
MEMBER[成员状态<br/>维护在线用户及频道位置]
|
||||
|
||||
MEMBER --> M0[空基线 Empty<br/>尚未执行 ListClients]
|
||||
MEMBER --> M1[已有基线 Loaded<br/>getClientList 已建立成员表]
|
||||
MEMBER --> M2[增量更新 Updating<br/>OnClientEnter/Leave/Moved 正在归并]
|
||||
MEMBER --> M3[不一致 Inconsistent<br/>事件引用未知成员或频道]
|
||||
|
||||
M0 --> M1
|
||||
M1 --> M2
|
||||
M2 --> M1
|
||||
M2 --> M3
|
||||
M3 --> M1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、成员实体状态树
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ENTITY[成员实体状态树<br/>描述 ListClients 基线与实时事件如何协同]
|
||||
|
||||
ENTITY --> BASE[基线分支<br/>由客户端请求建立完整成员集合]
|
||||
ENTITY --> DELTA[增量分支<br/>由服务端推送维护实时变化]
|
||||
ENTITY --> SELF[当前用户分支<br/>通过 ClientID 识别自己的事件]
|
||||
ENTITY --> REPAIR[修复分支<br/>处理事件缺失或引用不一致]
|
||||
|
||||
BASE --> B1[TSBridge.getClientsJSON 请求<br/>获取服务器当前全部在线客户端]
|
||||
B1 --> B2[按 clientID 建表<br/>使用客户端 ID 去重保存]
|
||||
B2 --> B3[按 channelID 建索引<br/>派生每个频道的成员列表]
|
||||
|
||||
DELTA --> D1[EventBus ClientEnter 事件<br/>debounce 合并后全量刷新]
|
||||
DELTA --> D2[EventBus ClientMoved 事件<br/>debounce 合并后全量刷新]
|
||||
DELTA --> D3[EventBus ClientLeave 事件<br/>debounce 合并后全量刷新]
|
||||
|
||||
SELF --> S1[TSBridge.getClientID 本地调用<br/>读取服务器分配的自身 ID]
|
||||
S1 --> S2[事件 clientID 比对<br/>判断移动或离开事件是否属于自己]
|
||||
S2 --> S3[更新自身频道事实<br/>自己的移动决定实际所在频道]
|
||||
|
||||
REPAIR --> R1[检测未知 clientID<br/>移动或离开事件找不到实体]
|
||||
R1 --> R2[重新调用 TSBridge.getClientsJSON<br/>用完整列表修复成员基线]
|
||||
REPAIR --> R3[检测未知 channelID<br/>成员引用本地不存在的频道]
|
||||
R3 --> R4[重新调用 TSBridge.getChannelsJSON<br/>用完整列表修复频道基线]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、时序:首次同步
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 上层应用<br/>发起同步并更新状态
|
||||
participant VM as ViewModel<br/>收集 EventBus 事件
|
||||
participant BRIDGE as TSBridge<br/>调用 gomobile API
|
||||
participant SDK as Go SDK (gomobile)<br/>发送列表请求
|
||||
participant TS as TeamSpeak 服务器<br/>返回频道和成员数据
|
||||
participant STORE as 业务状态仓库<br/>保存基线数据
|
||||
|
||||
Note over VM: EventBus Connected 事件触发首次同步
|
||||
|
||||
par 请求频道基线
|
||||
VM->>BRIDGE: TSBridge.getChannelsJSON()
|
||||
BRIDGE->>SDK: gomobile 调用 GetChannelsJSON
|
||||
SDK->>TS: channellist
|
||||
TS-->>SDK: JSON 字符串
|
||||
SDK-->>BRIDGE: 频道 JSON
|
||||
and 请求成员基线
|
||||
VM->>BRIDGE: TSBridge.getClientsJSON()
|
||||
BRIDGE->>SDK: gomobile 调用 GetClientsJSON
|
||||
SDK->>TS: clientlist
|
||||
TS-->>SDK: JSON 字符串
|
||||
SDK-->>BRIDGE: 成员 JSON
|
||||
and 读取自身 ID
|
||||
VM->>BRIDGE: TSBridge.getClientID()
|
||||
BRIDGE-->>VM: selfClientID
|
||||
end
|
||||
|
||||
alt 全部成功
|
||||
VM->>STORE: 原子提交频道、成员和自身 ID
|
||||
STORE-->>UI: 同步完成 → 状态进入 Synchronized
|
||||
else 任一失败
|
||||
VM->>STORE: 标记同步失败 → 状态进入 SyncFailed
|
||||
Note over UI: 允许重试,不进入业务就绪
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、时序:增量同步与补偿同步
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TS as TeamSpeak 服务器<br/>产生成员变化事件
|
||||
participant SDK as Go SDK (gomobile)<br/>解析 notify* 通知
|
||||
participant BRIDGE as TSBridge<br/>JNI 回调转 EventBus
|
||||
participant EB as EventBus<br/>事件合并与分发
|
||||
participant VM as ViewModel<br/>debounce 后刷新成员
|
||||
participant STORE as 成员实体仓库<br/>以 clientID 保存唯一成员
|
||||
participant UI as 频道树 UI<br/>派生人数和成员预览
|
||||
|
||||
Note over TS,UI: ② 增量同步 — 正常事件流
|
||||
|
||||
TS-->>SDK: notifycliententerview
|
||||
SDK->>BRIDGE: OnClientEnter(Client)
|
||||
BRIDGE->>EB: emit(ClientEnter)
|
||||
|
||||
TS-->>SDK: notifyclientmoved
|
||||
SDK->>BRIDGE: OnClientMoved(id, channelID)
|
||||
BRIDGE->>EB: emit(ClientMoved)
|
||||
|
||||
TS-->>SDK: notifyclientleftview
|
||||
SDK->>BRIDGE: OnClientLeave(id, reason)
|
||||
BRIDGE->>EB: emit(ClientLeave)
|
||||
|
||||
Note over EB: debounce 300ms 合并
|
||||
|
||||
EB->>VM: collect → refreshClientList()
|
||||
VM->>STORE: TSBridge.getClientsJSON() → 全量刷新
|
||||
|
||||
alt 刷新成功
|
||||
STORE-->>UI: 基线已更新
|
||||
else 数据不一致
|
||||
Note over VM: ③ 补偿同步 — 修复丢失的基线
|
||||
VM->>STORE: TSBridge.getChannelsJSON() → 修复频道基线
|
||||
STORE-->>UI: 基线已修复
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、时序:重连后全量同步
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as 上层应用<br/>处理重连流程
|
||||
participant VM as ViewModel<br/>管理清理和重建
|
||||
participant BRIDGE as TSBridge<br/>管理连接生命周期
|
||||
participant SDK as Go SDK (gomobile)<br/>重新建立连接
|
||||
participant STORE as 会话数据仓库<br/>保存会话临时事实
|
||||
|
||||
Note over UI: 网络恢复或手动重连
|
||||
|
||||
UI->>VM: 检测到需要重连
|
||||
VM->>VM: 状态改为 disconnecting
|
||||
VM->>STORE: 清理旧会话数据
|
||||
STORE-->>VM: 清理完成
|
||||
|
||||
Note over VM: 重新执行连接流程
|
||||
|
||||
VM->>BRIDGE: TSBridge.connect(...)
|
||||
BRIDGE->>SDK: gomobile Connect
|
||||
SDK-->>BRIDGE: OnConnected 回调
|
||||
BRIDGE->>BRIDGE: EventBus.emit(Connected)
|
||||
BRIDGE->>VM: EventBus 分发 Connected 事件
|
||||
|
||||
Note over VM: 重新执行首次同步
|
||||
|
||||
VM->>BRIDGE: getChannelsJSON + getClientsJSON + getClientID
|
||||
BRIDGE->>SDK: gomobile 调用
|
||||
SDK-->>BRIDGE: 完整基线数据
|
||||
VM->>STORE: 原子提交新基线
|
||||
STORE-->>UI: 同步完成 → 恢复业务就绪
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、同步与主流程的关系
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph 主流程触发点
|
||||
A[01 连接服务器<br/>EventBus Connected]
|
||||
B[02 浏览频道<br/>EventBus ClientEnter/Moved/Leave]
|
||||
C[03 切换频道<br/>EventBus ClientMoved]
|
||||
D[04 文本消息<br/>EventBus TextMessage]
|
||||
E[07 断开连接<br/>重新连接]
|
||||
end
|
||||
|
||||
subgraph 状态同步流程
|
||||
F[① 首次同步]
|
||||
G[② 增量同步]
|
||||
H[③ 补偿同步]
|
||||
I[④ 自身状态同步]
|
||||
J[⑤ 消息归档同步]
|
||||
K[⑥ 重连后全量同步]
|
||||
L[⑦ 同步失败处理]
|
||||
end
|
||||
|
||||
A --> F
|
||||
B --> G
|
||||
B --> H
|
||||
C --> I
|
||||
D --> J
|
||||
E --> K
|
||||
F --> L
|
||||
K --> F
|
||||
H --> F
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、事件依赖
|
||||
|
||||
### 同步相关前置依赖矩阵
|
||||
|
||||
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| 首次同步 | EventBus `Connected` 事件 | 同时读取频道、成员和自身 ID | 任一核心请求失败则不标记业务就绪 |
|
||||
| `getClientsJSON` 增量归并 | 已有频道基线更易校验 | 与 `getChannelsJSON` 同一同步批次 | 未知频道触发频道补偿读取 |
|
||||
| `ClientEnter` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
|
||||
| `ClientMoved` 事件 | 已注册 EventBus 事件收集 | 已有成员基线和自身 ID | debounce 后全量刷新 |
|
||||
| `ClientLeave` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
|
||||
| 提交自己新频道 | 自己的 `ClientMoved` 事件 | 匹配目标频道和请求上下文 | 命令响应不能直接提交频道 |
|
||||
| `TextMessage` 归档 | `targetMode`、`target`、发送者信息 | 已有频道或私聊实体 | 不得使用当前页面猜测目标 |
|
||||
|
||||
### 权威性划分
|
||||
|
||||
| 数据 | 推荐权威来源 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 初始频道列表 | `TSBridge.getChannelsJSON()` 响应 | SDK 未列出频道创建、更新、删除事件 |
|
||||
| 初始在线成员 | `TSBridge.getClientsJSON()` 响应 | 建立完整在线成员基线 |
|
||||
| 自身客户端 ID | `TSBridge.getClientID()` | SDK 本地缓存服务器分配的 ID |
|
||||
| 成员实时变化 | EventBus `ClientEnter/Leave/Moved` 事件 | 服务端主动推送实时事实,debounce 后全量刷新 |
|
||||
| 文本消息 | EventBus `TextMessage` 事件 | 来源是 `notifytextmessage` |
|
||||
|
||||
### 缺失事件能力带来的依赖限制
|
||||
|
||||
根据 SDK 文档,当前公开事件不包含频道创建、频道更新和频道删除。因此:
|
||||
|
||||
1. `TsClient.getChannelList()` 是频道目录的主要权威来源。
|
||||
2. 不能假设频道列表会依靠 `onClient*` 事件永久保持最新。
|
||||
3. 发现成员引用未知频道时,应重新调用 `TsClient.getChannelList()`。
|
||||
4. 如果产品需要实时频道管理,应扩展 SDK 对相应 `notify*` 通知的支持。
|
||||
|
||||
### 统一实现原则(同步相关)
|
||||
|
||||
- **列表命令建立基线,服务端事件维护增量。** 两者缺一不可。
|
||||
- **成员表按 ClientID 幂等更新。** debounce 合并后 refreshClientList 全量刷新,天然幂等。
|
||||
- **当前用户通过 ClientID 识别。** 不依赖昵称或 UI 当前页面判断自己。
|
||||
- **未知实体引用触发补偿同步。** 不静默忽略移动或离开事件。
|
||||
- **消息按 TargetMode 和 Target 归档。** 不使用当前页面频道代替真实目标。
|
||||
- **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
|
||||
- **事件通过 EventBus 分发,不在 JNI 回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`,ViewModel 通过 `collect()` 响应。
|
||||
@@ -0,0 +1,321 @@
|
||||
# EventBus 架构
|
||||
|
||||
> 本文档描述 TeamSpeak 事件总线(EventBus)的设计,将 TS 事件线程与 UI 渲染线程分离。
|
||||
> EventBus 是 TSBridge 回调与 ViewModel 状态更新之间的中间层,负责事件收集、合并、节流和分发。
|
||||
|
||||
---
|
||||
|
||||
## 一、设计目的
|
||||
|
||||
### 1.1 问题
|
||||
|
||||
当前架构中,Go JNI 回调直接触发 ViewModel 状态更新:
|
||||
|
||||
```
|
||||
Go goroutine → JNI callback → ServerViewModel (直接写 StateFlow)
|
||||
```
|
||||
|
||||
存在的问题:
|
||||
|
||||
1. **线程不一致**:JNI 回调在 Go goroutine 线程上执行,部分状态更新直接赋值(`_state.value = xxx`),部分通过 `viewModelScope.launch` 切到 Main,缺乏统一的线程策略。
|
||||
2. **事件风暴**:服务器连续推送多个 `onClientEnter` / `onClientMoved` 事件时,每次都触发 `refreshClientList()` 全量查询,造成不必要的命令开销和 UI 频繁重组。
|
||||
3. **关注点耦合**:TSBridge 回调直接依赖 ViewModel 实现,无法独立调试、日志记录或重放事件。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
```
|
||||
Go goroutine → JNI callback → TSBridge → [EventBus] → ViewModel → StateFlow → UI
|
||||
↑
|
||||
这一层负责:
|
||||
- 统一线程调度(全部切到 Main)
|
||||
- 合并高频事件(debounce)
|
||||
- 事件日志与调试
|
||||
- TS 线程完全不碰 UI 状态
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、线程模型全景
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph GO[Go Runtime]
|
||||
NET[Network goroutines<br/>receiveLoop / processLoop / resendLoop / pingLoop]
|
||||
EVT[事件消费 goroutine<br/>串行调用 JNI 回调]
|
||||
NET --> EVT
|
||||
end
|
||||
|
||||
subgraph KOTLIN[Kotlin / Android]
|
||||
subgraph IO[Dispatchers.IO 线程池]
|
||||
CAP[麦克风采集循环]
|
||||
PLAY[语音解码与播放]
|
||||
FETCH[JSON 解析<br/>fetchChannels / fetchClients]
|
||||
end
|
||||
|
||||
subgraph MAIN[Main Thread]
|
||||
VM[ViewModel 业务逻辑]
|
||||
SF[StateFlow 状态更新]
|
||||
UI[Compose UI 渲染]
|
||||
VM --> SF --> UI
|
||||
end
|
||||
|
||||
EB[EventBus<br/>事件收集 / 合并 / 分发]
|
||||
end
|
||||
|
||||
EVT -->|JNI 回调<br/>Go goroutine 线程| EB
|
||||
EB -->|Dispatchers.Main| VM
|
||||
CAP -->|Dispatchers.IO| EB
|
||||
FETCH -->|Dispatchers.IO| EB
|
||||
```
|
||||
|
||||
### 线程职责划分
|
||||
|
||||
| 线程 / 调度器 | 职责 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Go goroutines | 网络收发、协议处理、事件队列消费 | 双层事件队列保证 JNI 回调串行执行 |
|
||||
| JNI 回调线程 | 仅负责"把事件扔进 EventBus" | 不做任何状态更新或 UI 操作 |
|
||||
| Dispatchers.IO | 麦克风采集、语音播放、JSON 解析 | 阻塞 IO 操作 |
|
||||
| Dispatchers.Main | ViewModel 逻辑、StateFlow 更新、Compose 渲染 | 所有 UI 状态变更在主线程 |
|
||||
|
||||
---
|
||||
|
||||
## 三、事件定义
|
||||
|
||||
### 3.1 TSEvent 密封类
|
||||
|
||||
```kotlin
|
||||
sealed class TSEvent {
|
||||
// 连接生命周期
|
||||
object Connected : TSEvent()
|
||||
data class Disconnected(val message: String) : TSEvent()
|
||||
data class Kicked(val reason: String) : TSEvent()
|
||||
|
||||
// 成员变化
|
||||
data class ClientEnter(val client: Client) : TSEvent()
|
||||
data class ClientLeave(val id: Long, val reasonMsg: String) : TSEvent()
|
||||
data class ClientMoved(val id: Long, val targetChannelID: String) : TSEvent()
|
||||
|
||||
// 消息
|
||||
data class TextMessage(val msg: TextMsg) : TSEvent()
|
||||
|
||||
// 语音
|
||||
data class VoiceData(val clientID: Long, val data: ByteArray, val codec: Long) : TSEvent()
|
||||
|
||||
// 通知
|
||||
data class Poked(val event: TSBridge.PokeEventData) : TSEvent()
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 事件分类
|
||||
|
||||
| 类别 | 事件 | 处理策略 |
|
||||
| --- | --- | --- |
|
||||
| 连接生命周期 | Connected / Disconnected / Kicked | **立即分发** — 影响全局状态,不可延迟 |
|
||||
| 成员变化 | ClientEnter / ClientLeave / ClientMoved | **合并分发** — debounce 后触发一次 refreshClientList |
|
||||
| 消息 | TextMessage | **立即分发** — 用户期望实时看到新消息 |
|
||||
| 语音 | VoiceData | **IO 线程直接处理** — 不经过 EventBus,延迟敏感 |
|
||||
| 通知 | Poked | **立即分发** — 需要弹出通知 |
|
||||
|
||||
---
|
||||
|
||||
## 四、EventBus 实现
|
||||
|
||||
### 4.1 核心结构
|
||||
|
||||
```kotlin
|
||||
object EventBus {
|
||||
private val _events = MutableSharedFlow<TSEvent>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 64,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val events: SharedFlow<TSEvent> = _events.asSharedFlow()
|
||||
|
||||
/** 发送事件(可从任意线程调用) */
|
||||
fun emit(event: TSEvent) {
|
||||
_events.tryEmit(event)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 事件合并策略
|
||||
|
||||
对于高频成员变化事件,使用 debounce 合并:
|
||||
|
||||
```kotlin
|
||||
// 在 ViewModel 中收集事件时
|
||||
viewModelScope.launch {
|
||||
EventBus.events
|
||||
.filterIsInstance<TSEvent.ClientEnter>()
|
||||
.debounce(300) // 300ms 内的多次 ClientEnter 合并为一次
|
||||
.collect {
|
||||
Repository.refreshClientList()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
合并规则:
|
||||
|
||||
| 事件 | 合并策略 | 延迟 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| ClientEnter | debounce | 300ms | 连续多人进入时只刷新一次 |
|
||||
| ClientLeave | debounce | 300ms | 连续多人离开时只刷新一次 |
|
||||
| ClientMoved | debounce | 300ms | 连续多人移动时只刷新一次 |
|
||||
| Connected | 不合并 | 0 | 立即处理 |
|
||||
| Disconnected | 不合并 | 0 | 立即处理 |
|
||||
| Kicked | 不合并 | 0 | 立即处理 |
|
||||
| TextMessage | 不合并 | 0 | 立即归档 |
|
||||
| Poked | 不合并 | 0 | 立即弹出 |
|
||||
|
||||
### 4.3 TSBridge 回调注册
|
||||
|
||||
```kotlin
|
||||
// TSBridge.connect() 内部
|
||||
val callbackProxy = object : EventCallback {
|
||||
override fun onConnected() {
|
||||
EventBus.emit(TSEvent.Connected)
|
||||
}
|
||||
override fun onDisconnected(message: String) {
|
||||
EventBus.emit(TSEvent.Disconnected(message))
|
||||
}
|
||||
override fun onTextMessage(msg: TextMsg?) {
|
||||
msg?.let { EventBus.emit(TSEvent.TextMessage(it)) }
|
||||
}
|
||||
override fun onClientEnter(client: Client?) {
|
||||
client?.let { EventBus.emit(TSEvent.ClientEnter(it)) }
|
||||
}
|
||||
override fun onClientLeave(id: Long, reasonMsg: String) {
|
||||
EventBus.emit(TSEvent.ClientLeave(id, reasonMsg))
|
||||
}
|
||||
override fun onClientMoved(id: Long, targetChannelID: String) {
|
||||
EventBus.emit(TSEvent.ClientMoved(id, targetChannelID))
|
||||
}
|
||||
override fun onKicked(reason: String) {
|
||||
EventBus.emit(TSEvent.Kicked(reason))
|
||||
}
|
||||
override fun onVoiceData(clientID: Long, data: ByteArray?, codec: Long) {
|
||||
// 语音数据延迟敏感,不经过 EventBus,直接回调 VoiceService
|
||||
data?.let { callbacks.onVoiceData(clientID, it, codec) }
|
||||
}
|
||||
override fun onPoked(event: TSBridge.PokeEventData?) {
|
||||
event?.let { EventBus.emit(TSEvent.Poked(it)) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、事件流时序
|
||||
|
||||
### 5.1 成员变化事件流(合并)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant TS as TeamSpeak 服务器
|
||||
participant GO as Go 事件消费 goroutine
|
||||
participant JNI as JNI 回调
|
||||
participant EB as EventBus
|
||||
participant VM as ViewModel (Main)
|
||||
participant UI as Compose UI
|
||||
|
||||
TS-->>GO: notifycliententerview (客户端 A)
|
||||
GO->>JNI: OnClientEnter(A)
|
||||
JNI->>EB: emit(ClientEnter(A))
|
||||
|
||||
TS-->>GO: notifycliententerview (客户端 B)
|
||||
GO->>JNI: OnClientEnter(B)
|
||||
JNI->>EB: emit(ClientEnter(B))
|
||||
|
||||
TS-->>GO: notifyclientmoved (客户端 C)
|
||||
GO->>JNI: OnClientMoved(C)
|
||||
JNI->>EB: emit(ClientMoved(C))
|
||||
|
||||
Note over EB: debounce 300ms 合并
|
||||
|
||||
EB->>VM: collect → refreshClientList()
|
||||
VM->>UI: StateFlow 更新 → 一次性重组
|
||||
```
|
||||
|
||||
### 5.2 连接事件流(立即)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GO as Go 事件消费 goroutine
|
||||
participant JNI as JNI 回调
|
||||
participant EB as EventBus
|
||||
participant VM as ServerViewModel (Main)
|
||||
participant UI as Compose UI
|
||||
|
||||
GO->>JNI: OnConnected()
|
||||
JNI->>EB: emit(Connected)
|
||||
EB->>VM: collect → 处理连接成功
|
||||
VM->>VM: 执行首次同步
|
||||
VM->>UI: 连接状态更新
|
||||
```
|
||||
|
||||
### 5.3 语音数据流(绕过 EventBus)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GO as Go 事件消费 goroutine
|
||||
participant JNI as JNI 回调
|
||||
participant VS as VoiceService (IO)
|
||||
participant SP as 扬声器
|
||||
|
||||
GO->>JNI: OnVoiceData(clientID, data, codec)
|
||||
JNI->>VS: handleVoiceData() 直接回调
|
||||
VS->>VS: Opus 解码 (Dispatchers.IO)
|
||||
VS->>SP: AudioTrack.write()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、EventBus 与现有组件的关系
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 现有架构
|
||||
TSB[TSBridge<br/>JNI 回调注册]
|
||||
SVM[ServerViewModel<br/>连接 / 断开 / 重连]
|
||||
CVM[ChannelViewModel<br/>频道列表 / 切换]
|
||||
CHVM[ChatViewModel<br/>消息归档]
|
||||
VVM[VoiceViewModel<br/>语音控制]
|
||||
REPO[Repository<br/>状态仓库]
|
||||
end
|
||||
|
||||
subgraph 新增
|
||||
EB[EventBus<br/>事件收集 / 合并 / 分发]
|
||||
EVT[TSEvent 密封类<br/>事件类型定义]
|
||||
end
|
||||
|
||||
TSB -->|JNI 回调| EB
|
||||
EB -->|SharedFlow| SVM
|
||||
EB -->|SharedFlow| CVM
|
||||
EB -->|SharedFlow| CHVM
|
||||
EB -->|SharedFlow| VVM
|
||||
SVM --> REPO
|
||||
CVM --> REPO
|
||||
CHVM --> REPO
|
||||
|
||||
EB -.->|语音绕过| VVM
|
||||
```
|
||||
|
||||
### 各 ViewModel 监听的事件
|
||||
|
||||
| ViewModel | 监听事件 | 处理方式 |
|
||||
| --- | --- | --- |
|
||||
| ServerViewModel | Connected / Disconnected / Kicked | 立即更新连接状态 |
|
||||
| ChannelViewModel | ClientEnter / ClientLeave / ClientMoved | debounce 后 refreshClientList |
|
||||
| ChatViewModel | TextMessage | 立即归档消息 |
|
||||
| VoiceViewModel | — | 语音数据由 VoiceService 直接处理 |
|
||||
|
||||
---
|
||||
|
||||
## 七、与统一实现原则的关系
|
||||
|
||||
EventBus 架构强化了以下实现原则:
|
||||
|
||||
1. **事件通过 EventBus 分发,不在回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`,ViewModel 通过 `collect()` 响应。
|
||||
2. **列表命令建立基线,服务端事件维护增量。** EventBus 的 debounce 机制确保增量事件不会导致过度刷新。
|
||||
3. **成员表按 ClientID 幂等更新。** debounce 后的 refreshClientList 是全量刷新,天然幂等。
|
||||
4. **断开时统一清理当前会话资源。** Disconnected 事件通过 EventBus 统一分发,各 ViewModel 统一响应清理。
|
||||
Reference in New Issue
Block a user