Refactor LED control UI to Vue3
This commit is contained in:
+136
-62
@@ -2,48 +2,43 @@
|
||||
|
||||
## 目标
|
||||
|
||||
将一个前端大屏应用稳定投放到 `14880 x 3510` LED 大屏。
|
||||
本项目用于把一个逻辑尺寸为 `14880 x 3510` 的 LED 大屏内容,拆分到两台 GPU 输出服务器本地渲染。
|
||||
|
||||
硬件约束:
|
||||
生产形态:
|
||||
|
||||
- 两台 GPU 服务器。
|
||||
- 每台服务器 4 路 4K 输出。
|
||||
- 两台服务器品牌或 GPU 可能不同,无法依赖硬件级 GPU 同步。
|
||||
- 控制服务运行 FastAPI,负责场景、动作、时间同步、ACK、状态监控。
|
||||
- 左 GPU 服务器打开 `/output/left`,只显示左半屏 tile。
|
||||
- 右 GPU 服务器打开 `/output/right`,只显示右半屏 tile。
|
||||
- 每台 GPU 服务器输出 4 路 4K 到拼控或 LED 控制器。
|
||||
- 拼控或 LED 控制器完成最终物理拼接。
|
||||
|
||||
因此系统目标不是让两张 GPU 每一帧硬同步,而是:
|
||||
WebSocket 不传大视频流,只传状态、命令、时间、ACK 和性能指标。画面由两台 GPU 服务器本地渲染。
|
||||
|
||||
- 统一业务状态。
|
||||
- 统一提交时间。
|
||||
- 统一动画时间轴。
|
||||
- 输出端本地渲染。
|
||||
- 拼控完成物理拼接和输入对齐。
|
||||
|
||||
## 推荐架构
|
||||
## 逻辑拓扑
|
||||
|
||||
```text
|
||||
控制服务 FastAPI
|
||||
- 场景管理
|
||||
- WebSocket 同步
|
||||
- 时间校准
|
||||
- ACK 追踪
|
||||
- 性能监控
|
||||
|
||||
左 GPU 服务器
|
||||
- 打开 /output/left
|
||||
- 渲染完整大屏应用的 left tile
|
||||
- 4 路 4K 输出到拼控
|
||||
|
||||
右 GPU 服务器
|
||||
- 打开 /output/right
|
||||
- 渲染完整大屏应用的 right tile
|
||||
- 4 路 4K 输出到拼控
|
||||
|
||||
拼控 / LED 控制器
|
||||
- 接收 8 路 4K
|
||||
- 按物理坐标拼接成 14880 x 3510
|
||||
控制台 /
|
||||
http://control:8000/
|
||||
|
|
||||
v
|
||||
FastAPI 控制服务
|
||||
场景管理 / WebSocket Hub / SyncCoordinator / 状态接口
|
||||
| |
|
||||
prepare/commit/action status/telemetry/ACK
|
||||
| |
|
||||
-------------------------------
|
||||
| |
|
||||
v v
|
||||
左 GPU 输出服务器 右 GPU 输出服务器
|
||||
/output/left /output/right
|
||||
left tile 本地渲染 right tile 本地渲染
|
||||
4 路 4K 输出 4 路 4K 输出
|
||||
| |
|
||||
---------> 拼控 / LED 控制器 <-
|
||||
14880 x 3510
|
||||
```
|
||||
|
||||
## 左右屏如何分开
|
||||
## Tile 切分
|
||||
|
||||
完整逻辑画面:
|
||||
|
||||
@@ -56,13 +51,19 @@
|
||||
|
||||
```text
|
||||
left:
|
||||
x=0, y=0, width=7440, height=3510
|
||||
x=0
|
||||
y=0
|
||||
width=7440
|
||||
height=3510
|
||||
|
||||
right:
|
||||
x=7440, y=0, width=7440, height=3510
|
||||
x=7440
|
||||
y=0
|
||||
width=7440
|
||||
height=3510
|
||||
```
|
||||
|
||||
输出端页面创建完整逻辑大屏坐标系,然后根据自己的 tile 做视口偏移。真实 Three.js 项目中,应使用:
|
||||
输出端页面使用完整逻辑大屏坐标系,再根据自己的 tile 做视口偏移。Three.js 项目中建议使用:
|
||||
|
||||
```js
|
||||
camera.setViewOffset(
|
||||
@@ -71,19 +72,19 @@ camera.setViewOffset(
|
||||
tile.x,
|
||||
tile.y,
|
||||
tile.width,
|
||||
tile.height
|
||||
tile.height,
|
||||
);
|
||||
```
|
||||
|
||||
## 每台服务器 4 路 4K
|
||||
|
||||
每台服务器建议配置为 `2 x 2` 逻辑桌面:
|
||||
每台 GPU 服务器建议配置为 `2 x 2` 逻辑桌面:
|
||||
|
||||
```text
|
||||
7680 x 4320
|
||||
```
|
||||
|
||||
四路输出:
|
||||
四路输出映射:
|
||||
|
||||
```text
|
||||
1: x=0, y=0, 3840 x 2160
|
||||
@@ -96,35 +97,50 @@ camera.setViewOffset(
|
||||
|
||||
## 同步协议
|
||||
|
||||
场景切换:
|
||||
### 场景切换
|
||||
|
||||
控制台调用:
|
||||
|
||||
```text
|
||||
POST /api/scenes/{scene_id}/switch
|
||||
```
|
||||
|
||||
服务端广播:
|
||||
服务端生成同一个 `command_id`,先广播:
|
||||
|
||||
```text
|
||||
prepare_scene(command_id, apply_at_ms)
|
||||
commit_scene(command_id, apply_at_ms)
|
||||
prepare_scene(command_id, apply_at_ms, state, scene)
|
||||
```
|
||||
|
||||
输出端流程:
|
||||
输出端收到 `prepare_scene` 后:
|
||||
|
||||
1. 预加载资源。
|
||||
2. 等待至少两个 RAF,让浏览器完成布局和首帧准备。
|
||||
3. 如果存在 `window.ledPlatformPrepareScene`,等待业务方的真实预渲染/超分完成。
|
||||
4. 回 ACK:`status=prepared`。
|
||||
|
||||
服务端 `SyncCoordinator` 等 `target_tiles` 中的 `left` 和 `right` 都返回 `prepared` 后,才广播:
|
||||
|
||||
```text
|
||||
1. 收到 prepare,记录命令,可预加载资源。
|
||||
2. 收到 commit,等到 apply_at_ms。
|
||||
3. 到点提交场景。
|
||||
4. 回 ACK。
|
||||
commit_scene(command_id, apply_at_ms, state, scene)
|
||||
```
|
||||
|
||||
局部动作:
|
||||
输出端收到 `commit_scene` 后,等到统一的 `apply_at_ms` 再切换画面,并回 ACK:
|
||||
|
||||
```text
|
||||
status=committed
|
||||
```
|
||||
|
||||
这样谁渲染慢就等谁,两个输出端都准备好后才同步放行。
|
||||
|
||||
### 局部动作
|
||||
|
||||
控制台调用:
|
||||
|
||||
```text
|
||||
POST /api/actions
|
||||
```
|
||||
|
||||
支持结构化动作:
|
||||
支持动作:
|
||||
|
||||
```text
|
||||
page.next
|
||||
@@ -135,37 +151,93 @@ timeline.pause
|
||||
timeline.resume
|
||||
```
|
||||
|
||||
## 关键渲染原则
|
||||
局部动作目前直接按 `apply_at_ms` 调度并回 ACK:
|
||||
|
||||
所有动画、Three.js、地图和视频都应基于统一时间轴:
|
||||
```text
|
||||
status=action_committed
|
||||
```
|
||||
|
||||
如果后续某些局部动作也需要超分或重资源准备,可以复用场景切换的 prepare/barrier/commit 模式。
|
||||
|
||||
## 画面回显
|
||||
|
||||
控制台内置两个只读预览 iframe,并把它们无缝拼成一块完整画面:
|
||||
|
||||
```text
|
||||
/output/left?preview=1
|
||||
/output/right?preview=1
|
||||
```
|
||||
|
||||
预览模式通过 `/ws/admin` 接收状态和命令,不连接 `/ws/output/{tile_id}`,因此:
|
||||
|
||||
- 不注册为真实输出节点。
|
||||
- 不发送 `prepared`、`committed`、`telemetry` ACK。
|
||||
- 不会提前释放同步 barrier。
|
||||
- 只用于控制台观察画面。
|
||||
|
||||
真实生产输出端仍然打开:
|
||||
|
||||
```text
|
||||
/output/left
|
||||
/output/right
|
||||
```
|
||||
|
||||
## 时间同步
|
||||
|
||||
输出端通过 `clock_ping` / `clock_pong` 估算服务端时间:
|
||||
|
||||
```text
|
||||
serverNowMs = Date.now() + serverOffsetFromDateMs
|
||||
```
|
||||
|
||||
RTT 使用 `performance.now()` 估算,服务端时间偏移使用客户端 wall clock 的发送时间和返回时间中点估算。
|
||||
|
||||
渲染和动画应基于统一时间轴:
|
||||
|
||||
```js
|
||||
const t = serverNowMs() - sceneStartedAtMs;
|
||||
renderSceneAt(t);
|
||||
```
|
||||
|
||||
不要让左右服务器各自自由播放:
|
||||
不要让左右输出端各自累计本地 delta:
|
||||
|
||||
```js
|
||||
// 不推荐
|
||||
animation += localDeltaTime;
|
||||
```
|
||||
|
||||
这样即使某台机器偶尔慢一帧,也会在下一帧追到统一时间,不会越播越偏。
|
||||
## 状态观测
|
||||
|
||||
## 为什么不推荐单机渲染后网络分发
|
||||
控制台显示:
|
||||
|
||||
单侧半屏未压缩数据量:
|
||||
- 输出节点数量。
|
||||
- 每个输出节点 FPS、frame time、RTT、clock offset。
|
||||
- 最近命令回显。
|
||||
- 每个 tile 的 `prepared`、`committed`、`late`、`error` 状态。
|
||||
- barrier 放行耗时。
|
||||
|
||||
接口:
|
||||
|
||||
```text
|
||||
7440 x 3510 x 4 bytes x 60fps ~= 6.3 GB/s
|
||||
GET /api/sync/status
|
||||
GET /healthz
|
||||
```
|
||||
|
||||
左右两侧合计超过 `12 GB/s`。普通网络视频流需要编码、传输、解码,会带来延迟、画质损失、文字细线压缩失真,以及新的编码/解码同步问题。
|
||||
## 为什么不传视频流
|
||||
|
||||
单侧半屏未压缩数据量约为:
|
||||
|
||||
```text
|
||||
7440 x 3510 x 4 bytes x 60 fps ~= 6.3 GB/s
|
||||
```
|
||||
|
||||
左右两侧合计超过 `12 GB/s`。普通网络视频流还需要编码、传输、解码,会带来延迟、画质损失、文字细线压缩失真,以及新的编码/解码同步问题。
|
||||
|
||||
本系统选择“控制服务发命令,输出端本地渲染”,更适合高分辨率 LED 墙。
|
||||
|
||||
## 生产验收指标
|
||||
|
||||
建议关注 P95/P99,而不是平均值:
|
||||
建议关注 P95/P99:
|
||||
|
||||
```text
|
||||
60 FPS:
|
||||
@@ -182,6 +254,7 @@ animation += localDeltaTime;
|
||||
```text
|
||||
WebSocket RTT < 10ms
|
||||
clock offset < 5ms
|
||||
left/right 都返回 prepared
|
||||
left/right 都返回 committed 或 action_committed
|
||||
不得频繁出现 late ACK
|
||||
```
|
||||
@@ -199,12 +272,13 @@ left/right 都返回 committed 或 action_committed
|
||||
能保证:
|
||||
|
||||
- 两边业务状态一致。
|
||||
- 两边按同一服务端时间点提交。
|
||||
- 两边都准备好后才释放场景提交。
|
||||
- 两边按统一服务端时间点提交。
|
||||
- 动画长期不漂移。
|
||||
- 命令和性能可观测。
|
||||
|
||||
不能单独保证:
|
||||
|
||||
- 两张不同 GPU 每一帧物理扫描完全同相。
|
||||
- 两张不同 GPU 的物理扫描完全同相。
|
||||
|
||||
如果必须达到广播级帧同步,需要硬件层支持 Genlock / Frame Lock / 专业视频墙控制器。
|
||||
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
# 使用文档
|
||||
|
||||
## 1. 环境要求
|
||||
|
||||
- Python `3.10` 到 `3.12` 推荐。
|
||||
- macOS 本机如果没有 `python` 命令,使用 `python3` 或明确的 Python 3.12 路径。
|
||||
- 浏览器建议使用 Chrome / Chromium / Edge。
|
||||
- 生产输出服务器建议关闭休眠、屏保和自动更新弹窗。
|
||||
|
||||
当前项目依赖见:
|
||||
|
||||
```text
|
||||
requirements.txt
|
||||
pyproject.toml
|
||||
```
|
||||
|
||||
## 2. 首次启动
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
cd /Users/tjy/Documents/code/work/dp
|
||||
/Users/tjy/.local/bin/python3.12 -m venv .venv
|
||||
.venv/bin/python -m pip install -r requirements.txt
|
||||
.venv/bin/python -m led_platform.cli serve
|
||||
```
|
||||
|
||||
服务默认监听:
|
||||
|
||||
```text
|
||||
0.0.0.0:8000
|
||||
```
|
||||
|
||||
如果要指定端口:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m led_platform.cli serve --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
## 3. 日常启动
|
||||
|
||||
```bash
|
||||
cd /Users/tjy/Documents/code/work/dp
|
||||
.venv/bin/python -m led_platform.cli serve
|
||||
```
|
||||
|
||||
查看本机局域网地址:
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m led_platform.cli urls
|
||||
```
|
||||
|
||||
也可以直接查看:
|
||||
|
||||
```bash
|
||||
ipconfig getifaddr en0
|
||||
```
|
||||
|
||||
## 4. 常用地址
|
||||
|
||||
本机访问:
|
||||
|
||||
```text
|
||||
控制台: http://127.0.0.1:8000/
|
||||
左输出: http://127.0.0.1:8000/output/left
|
||||
右输出: http://127.0.0.1:8000/output/right
|
||||
健康检查: http://127.0.0.1:8000/healthz
|
||||
同步状态: http://127.0.0.1:8000/api/sync/status
|
||||
API 文档: http://127.0.0.1:8000/docs
|
||||
```
|
||||
|
||||
局域网访问时,把 `127.0.0.1` 换成控制服务 IP,例如:
|
||||
|
||||
```text
|
||||
http://192.168.0.182:8000/
|
||||
```
|
||||
|
||||
## 5. 控制台使用
|
||||
|
||||
打开:
|
||||
|
||||
```text
|
||||
http://控制服务IP:8000/
|
||||
```
|
||||
|
||||
控制台包含:
|
||||
|
||||
- 画面回显:把 left/right 两个只读预览无缝拼成一块完整画面,桌面布局下位于控制台顶部。
|
||||
- 场景切换:切换 overview、energy、security。
|
||||
- 输出与同步:查看输出端连接数量、FPS、frame time、RTT、clock offset。
|
||||
- 生产命令回显:查看真实 `/output/left` 和 `/output/right` 的 prepared、放行、committed 状态。
|
||||
- 局部动作:翻页、能源模式、告警、暂停/恢复时间轴。
|
||||
|
||||
画面回显使用:
|
||||
|
||||
```text
|
||||
/output/left?preview=1
|
||||
/output/right?preview=1
|
||||
```
|
||||
|
||||
控制台会把两个预览 iframe 贴在一起,形成 `14880 x 3510` 的完整大屏回显。它是只读预览,不会参与真实输出端 ACK。所以上方画面变化只代表控制台预览已更新;生产命令回显里的等待、放行和完成,代表真实输出端是否已经 ACK。
|
||||
|
||||
## 6. 生产输出端使用
|
||||
|
||||
左 GPU 输出服务器打开:
|
||||
|
||||
```text
|
||||
http://控制服务IP:8000/output/left
|
||||
```
|
||||
|
||||
右 GPU 输出服务器打开:
|
||||
|
||||
```text
|
||||
http://控制服务IP:8000/output/right
|
||||
```
|
||||
|
||||
输出端打开后会:
|
||||
|
||||
1. 请求 `/api/bootstrap/{tile_id}` 获取 tile、场景和当前状态。
|
||||
2. 连接 `/ws/output/{tile_id}`。
|
||||
3. 上报 `hello`,注册为真实输出节点。
|
||||
4. 定时进行时钟同步。
|
||||
5. 定时上报 FPS、frame time、dropped frames。
|
||||
6. 收到场景命令后执行 prepare/barrier/commit。
|
||||
|
||||
## 7. 场景切换流程
|
||||
|
||||
控制台点击“切换”后:
|
||||
|
||||
1. 后端生成新场景状态和 `command_id`。
|
||||
2. 后端广播 `prepare_scene`。
|
||||
3. left/right 真实输出端预加载或预渲染。
|
||||
4. left/right 都回 `prepared`。
|
||||
5. 后端记录 `released_at_ms` 并广播 `commit_scene`。
|
||||
6. left/right 等到 `apply_at_ms` 同步显示。
|
||||
7. left/right 回 `committed` 或 `late`。
|
||||
|
||||
在控制台“命令回显”里可以看到每个阶段。
|
||||
|
||||
## 8. 接入真实超分渲染
|
||||
|
||||
输出端预留了 hook:
|
||||
|
||||
```js
|
||||
window.ledPlatformPrepareScene = async ({ commandId, tile, state, scene }) => {
|
||||
// 1. 根据 scene/state 准备真实资源
|
||||
// 2. 执行超分或离屏渲染
|
||||
// 3. 确认下一次 commit 可以无卡顿显示
|
||||
};
|
||||
```
|
||||
|
||||
只要这个 Promise 不 resolve,输出端就不会发送 `prepared`。后端也就不会释放 `commit_scene`。
|
||||
|
||||
如果超分失败,输出端会回:
|
||||
|
||||
```text
|
||||
status=error
|
||||
```
|
||||
|
||||
## 9. 配置文件
|
||||
|
||||
场景配置:
|
||||
|
||||
```text
|
||||
config/scenes.json
|
||||
```
|
||||
|
||||
字段:
|
||||
|
||||
```text
|
||||
id 场景 ID
|
||||
name 控制台显示名
|
||||
url 场景 URL,目前示例都使用 /wall-app
|
||||
view 输出端内部视图名
|
||||
description 描述
|
||||
preload 准备阶段预加载资源
|
||||
```
|
||||
|
||||
Tile 配置:
|
||||
|
||||
```text
|
||||
config/tiles.json
|
||||
```
|
||||
|
||||
字段:
|
||||
|
||||
```text
|
||||
id left / right
|
||||
x, y 在完整逻辑大屏中的起点
|
||||
width, height tile 尺寸
|
||||
wall_width 完整大屏宽度
|
||||
wall_height 完整大屏高度
|
||||
desktop_width 单台 GPU 服务器桌面宽度
|
||||
desktop_height 单台 GPU 服务器桌面高度
|
||||
physical_outputs 4 路 4K 输出映射
|
||||
```
|
||||
|
||||
环境变量前缀:
|
||||
|
||||
```text
|
||||
LED_
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
LED_APP_PORT=9000 .venv/bin/python -m led_platform.cli serve
|
||||
LED_SWITCH_PREPARE_DELAY_MS=3000 .venv/bin/python -m led_platform.cli serve
|
||||
```
|
||||
|
||||
## 10. 验证命令
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest -q
|
||||
.venv/bin/python -m compileall led_platform tests
|
||||
node --check led_platform/web/static/output/main.js
|
||||
node --check led_platform/web/static/admin/main.js
|
||||
```
|
||||
|
||||
健康检查:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/healthz
|
||||
```
|
||||
|
||||
同步状态:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8000/api/sync/status
|
||||
```
|
||||
|
||||
## 11. 常见问题
|
||||
|
||||
### 控制台看不到画面回显
|
||||
|
||||
先强制刷新控制台页面。画面回显在控制台顶部,标题为“画面回显”。
|
||||
|
||||
如果仍然看不到,检查:
|
||||
|
||||
```text
|
||||
http://控制服务IP:8000/output/left?preview=1
|
||||
http://控制服务IP:8000/output/right?preview=1
|
||||
```
|
||||
|
||||
这两个地址应能单独打开预览画面。
|
||||
|
||||
### 命令回显一直显示等待
|
||||
|
||||
检查真实输出端是否打开的是:
|
||||
|
||||
```text
|
||||
/output/left
|
||||
/output/right
|
||||
```
|
||||
|
||||
不要把生产输出端打开成 `?preview=1`,预览模式不会 ACK。
|
||||
|
||||
### 切换场景不放行
|
||||
|
||||
说明至少一个真实输出端没有回 `prepared`。查看:
|
||||
|
||||
```text
|
||||
/api/sync/status
|
||||
```
|
||||
|
||||
重点看 `nodes` 是否有 left/right,`commands[].acks` 是否有两个 tile。
|
||||
|
||||
### 局域网机器打不开
|
||||
|
||||
检查:
|
||||
|
||||
- 服务是否监听 `0.0.0.0:8000`。
|
||||
- 控制服务机器防火墙是否允许 8000。
|
||||
- 输出服务器是否和控制服务在同一网络。
|
||||
- URL 是否使用控制服务的局域网 IP,而不是 `127.0.0.1`。
|
||||
|
||||
### 控制台预览和生产输出不同步
|
||||
|
||||
控制台预览是观察用途,运行在控制台浏览器中,不参与 ACK,也不代表生产输出端性能。最终验收应以真实 `/output/left` 和 `/output/right` 为准。
|
||||
Reference in New Issue
Block a user