# 画布性能与媒体分发最终改造方案 状态:基于当前 `main`(`7bf655e9`)和现有 OSS/CDN 重构重新整理。本文是当前仓库的最终目标和一次性改造边界,不把已经存在的能力重复当成待建设,也不把关键问题推到二期、三期。 ## 1. 最终结论 当前仓库已经完成了画布优化的第一代基础设施: - 资源访问已经收敛到 `assets.ResolveAccess` 和 `POST /api/resources/access`。 - 画布已经有 `canvas-live-viewport` 的合成层预览、Leafer underlay/overlay 图形层、空间索引、节点拖拽预览和选择预览。 - 图片、视频和音频的多数展示路径已经使用 `storageKey` 解析访问地址。 - 云资源 `/file` 路径已经能够重定向到 CDN/源站,平台不必转发云端媒体正文。 因此,最终改造方向不是重写成 WebGL,也不是复制 LibTV 的组件,而是把当前已经存在的能力合并成一个闭环: ```text 稳定资源身份 -> 统一访问描述 -> poster-first 媒体状态机 -> 展示与字节读取分离 -> 交互预览与持久化提交分离 -> 可见节点 / 轻量节点 / 完整节点三层 LOD -> 同一帧节点、连接、对齐线更新 -> 以设备帧预算驱动调度和验收 ``` 用户手势帧永远不承担以下工作:整视频 Blob 下载、视频首帧编码上传、批量 React 复杂节点挂载、全量连线重算、逐 pointermove 持久化和签名地址同步写入画布。 ## 2. 外部方案需要修正的地方 外部方案仍然正确的核心原则是视频默认图片化、旧画面不清空、交互期间不做重活、屏内节点不能被屏外预算截断,以及节点与连线使用同一帧预览。 以下内容不能直接照搬: 1. **OSS snapshot 尚未成为当前仓库的统一首帧供应链。** 当前 `web/src/services/canvas-video-preview.ts` 仍在缺少持久 poster 时使用浏览器截帧并后台上传;这只能是历史资源的降级路径,不能继续作为新资源的正常路径。 2. **当前没有完整的 `SelfVirtualizingNode` 和渐进挂载队列。** `use-canvas-render-model.ts` 主要负责空间查询和 DOM 节点挂载窗口,固定使用 280/420/720 节点预算;这不是完整 LOD,也不能宣称支持任意万级画布。 3. **Leafer 目前主要承载连接、选框、对齐线和草稿线。** 节点正文仍是 DOM。`canvas-live-viewport.ts` 已解决交互期合成变换,但不能替代节点 LOD 和挂载调度。 4. **小地图仍然为所有节点创建 DOM 和标签。** 只有图片预览数量限制为 24,节点和标签没有虚拟化;节点规模增大时,小地图本身会变成主线程负担。 5. **当前播放后整文件缓存会制造重复流量。** `resource-blob-cache.ts` 曾由播放器播放后延迟 4 秒拉取完整 Blob;播放 Range 和完整下载可能并存,且缓存预算最高可达 2 GiB。这不属于普通展示行为。 6. **`provider-input` 的浏览器边界与文档不一致。** 当前登录用户 `/resources/access` 直接以 `allowProvider=true` 处理该用途,前端可以申请长 TTL 模型输入地址。最终合同必须把模型输入迁移到服务端受控读取,浏览器公开 batch 只接受展示、复制、下载和浏览器字节处理。 7. **固定数字不是性能承诺。** 0.23/0.27、256px、30+4、280/420/720、5000 条连接、DPR 上限 3 都只能是初始参数。最终参数必须由真实设备和帧预算测量得到。 ## 3. 最终分层 ### 3.1 持久数据层 画布 JSON 只保存稳定身份和业务元数据: ```ts type CanvasMediaRef = { kind: "image" | "video" | "audio"; resourceId?: string; storageKey?: string; // resource: originalUrl?: string; // 外部资源历史兼容字段 previewContent?: string; // 已经持久化的静态预览 videoPreview?: { storageKey?: string; content?: string; width?: number; height?: number; mimeType?: string; }; naturalWidth?: number; naturalHeight?: number; mimeType?: string; }; ``` 临时签名 URL、`blob:` URL、当前播放器实例、缓存 lease 和访问描述不得写入画布 JSON、任务结果、localStorage 或日志。 上传、生成、导入三条路径必须在创建节点时统一写入同一 `resource:`。只有外部直链未转存时才保留外部 URL,并明确标记为外部资源。 ### 3.2 访问描述层 `assets.ResolveAccess` 是唯一分发策略入口。浏览器端的 `media-access` 只负责: - 按 scope、资源、用途、变体合并请求。 - 使用服务端返回的 `expiresAt`、`refreshAt` 和 `revision`。 - 账号切换时取消在途请求并清理旧描述。 - 只把描述交给 ``、`