PersonalWebApplication/docs/superpowers/specs/2026-06-29-special-folder-video-media-design.md

236 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 设计:特殊文件夹视频媒体(上传后服务器压缩)
> 在特殊文件夹共享照片墙(`#<文件夹名>`)里支持视频作为墙上媒体元素。
> 视频上传后先显示处理中占位卡,服务器用 `ffmpeg` 压缩为低带宽 MP4完成后在墙上播放。
> 本设计只开放给特殊文件夹;个人 `#photo` 暂不开放视频入口。
前置阅读:
- [photo-wall.md](../../photo-wall.md)
- [2026-06-24-special-folders-design.md](2026-06-24-special-folders-design.md)
- [2026-06-24-photo-wall-server-storage-design.md](2026-06-24-photo-wall-server-storage-design.md)
## 1. 目标与范围
目标:
- 特殊文件夹支持图片和视频混合展示。
- 视频像照片一样可以上传、拖拽、缩放、置顶/置底、删除。
- 视频上传后不阻塞页面等待转码墙上立即出现“AI 正在压缩视频...”风格的处理中卡片。
- 服务器将常见视频格式统一压缩为适合 3M 带宽播放的 MP4。
- 不提供视频下载入口,不添加下载按钮。
非目标:
- 个人 `#photo` 视频上传。
- 多清晰度转码、自适应码率、HLS/DASH。
- 视频封面生成、时间轴截图、在线播放统计。
- 大视频断点续传。
## 2. 总体架构
前端把 `PhotoWallPage` 从“照片元素”扩展为“媒体元素”,新增 `video` 类型。特殊文件夹使用新的媒体 API 加载图片和视频;图片仍按现有视觉展示,视频按状态渲染为处理中卡片、失败卡片或 `<video controls preload="metadata">` 播放器。
后端把特殊文件夹存储从“图片列表”升级为“媒体列表”。图片可以同步保存并立即进入 `ready` 状态。视频上传后先写入临时文件,创建状态文件,后台调用 `ffmpeg` 转码;转码完成后只保留压缩后的 `.mp4` 和状态文件,删除原始临时文件。
`ffmpeg -movflags +faststart` 只用于优化浏览器在线播放体验:把 MP4 索引移动到文件开头,让 `<video>` 不必等完整文件下载完才开始播放。UI 不提供下载功能。
## 3. 媒体模型
特殊文件夹媒体项:
```ts
type MediaType = 'image' | 'video'
type MediaStatus = 'ready' | 'processing' | 'failed'
interface FolderMediaItem {
id: string
type: MediaType
status: MediaStatus
}
```
服务器为视频维护状态文件,建议路径为 `SPECIAL_DIR/<folder>/media/<id>.json`
```json
{
"id": "a1b2c3d4e5f60708",
"type": "video",
"status": "processing",
"originalName": "trip.mov",
"createdAt": "2026-06-29T12:34:56Z",
"outputFile": "a1b2c3d4e5f60708.mp4",
"error": null
}
```
状态语义:
- `processing`:原视频已接收,后台正在转码。前端显示处理中占位卡并轮询状态。
- `ready`:媒体可通过 `GET /media/{id}` 读取。图片返回原图,视频返回压缩后的 MP4。
- `failed`:转码失败。前端显示失败卡片,允许删除后重传。
## 4. 后端接口
新增特殊文件夹媒体接口,旧 `/photos*` 接口先保留,前端特殊文件夹切到 `/media*`
| 方法 | 路径 | 鉴权 | 行为 |
|---|---|---|---|
| GET | `/api/web/folder/{name}/media` | AuthUser + 白名单 | 列出图片和视频,返回 `{ items, max, totalBytes, overQuota }` |
| POST | `/api/web/folder/{name}/media` | AuthUser + 白名单 | multipart 上传一个图片或视频 |
| GET | `/api/web/folder/{name}/media/{id}` | AuthUser + 白名单 | 读取 `ready` 媒体字节 |
| GET | `/api/web/folder/{name}/media/{id}/status` | AuthUser + 白名单 | 读取单个媒体状态 |
| DELETE | `/api/web/folder/{name}/media/{id}` | AuthUser + 白名单 | 删除媒体文件、临时文件和状态文件 |
上传响应:
```json
{
"id": "a1b2c3d4e5f60708",
"type": "video",
"status": "processing"
}
```
错误码:
- `403`:用户无权访问该特殊文件夹。
- `404`:文件夹或媒体不存在。
- `409`:达到特殊文件夹元素上限。
- `413`:上传体或文件过大。
- `415`:不支持的媒体格式。
- `500`IO 或转码任务启动失败。
## 5. 上传与格式
视频上传使用 multipart而不是 data URL。原因是视频文件可能很大data URL 会额外增加约三分之一体积并迫使前后端把大文件整体放进内存。multipart 更适合 500MB 级文件。
支持的输入:
- 图片:沿用现有 `jpg/png/gif/webp`
- 视频:支持常见格式,至少包括 `mp4/mov/m4v/webm/avi/mkv`
约束:
- 单个原始视频最大 500MB。
- 单个图片仍沿用 8MiB 上限。
- 同一个特殊文件夹仍受总元素上限约束,图片、视频、装饰合计不超过当前 `MAX_ITEMS`
- 视频上传不限制时长。
## 6. 视频转码
运行时依赖:服务器必须安装 `ffmpeg`,并保证 Rust 服务进程可以调用。
转码目标适配 3M 服务器带宽:
- 容器MP4。
- 视频编码H.264。
- 音频编码AAC。
- 输出限制到 720p 档:横屏最大 1280x720竖屏最大 720x1280保持原方向不放大小视频。
- 帧率限制到 24fps。
- 视频码率目标约 900kbps缓冲 1800kbps。
- 音频码率 96kbps。
- `+faststart` 优化在线播放。
建议 ffmpeg 参数方向:
```bash
ffmpeg -y -i input \
-vf "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(ih,iw),min(1280,ih),-2)',fps=24" \
-c:v libx264 -preset veryfast -crf 30 -maxrate 900k -bufsize 1800k \
-c:a aac -b:a 96k \
-movflags +faststart output.mp4
```
并发策略:
- Rust 进程内视频转码并发限制为 1。
- 上传请求只负责接收文件、创建状态、启动后台任务,然后立即返回。
- 后台任务成功后把状态改为 `ready`,删除临时原视频。
- 后台任务失败后把状态改为 `failed`,删除临时原视频,保留短错误摘要。
## 7. 前端交互
特殊文件夹工具栏把“上传照片”改为“上传媒体”。选择文件、拖拽文件都支持图片和视频;剪贴板仍只要求保持图片粘贴可用,视频粘贴不是本期目标。
渲染规则:
- `image + ready`:沿用当前照片卡片。
- `video + processing`显示软木板风格占位卡文案为“AI 正在压缩视频...”。
- `video + ready`:显示带控件的 `<video controls preload="metadata">`,不自动播放。
- `video + failed`:显示“视频处理失败”,允许右键删除。
视频卡片行为:
- 支持拖拽、缩放、旋转、置顶/置底、删除。
- 双击编辑说明文字,逻辑与照片 caption 一致。
- 用户主动点击才播放,避免多个视频同时消耗 3M 带宽。
- 不显示下载按钮。
轮询:
- 上传视频返回 `processing` 后,前端创建本地元素并开始轮询 `/status`
- 轮询间隔建议 3 秒。
- 状态变为 `ready` 后获取 object URL 并替换播放器。
- 状态变为 `failed` 后停止轮询并显示失败卡。
- 组件卸载时停止轮询并释放 object URL。
## 8. 布局与状态兼容
当前特殊文件夹共享状态使用 `photoLayout`。本功能升级为通用 `mediaLayout`
```ts
interface MediaLayout {
x: number
y: number
rot: number
scale: number
z: number
w: number
caption: string
}
interface WallState {
bgIndex: number
decorations: El[]
mediaLayout: Record<string, MediaLayout>
}
```
兼容策略:
- 读取状态时,如果存在 `mediaLayout`,优先使用。
- 如果只有旧的 `photoLayout`,把它当作 `mediaLayout` 读取。
- 保存状态时写 `mediaLayout`;可以同时保留 `photoLayout` 一段时间以兼容旧前端,但新前端不再依赖它。
- 已有图片布局不丢失;新视频布局按同一媒体 id 保存。
## 9. 删除与清理
删除媒体时:
- `ready image`:删除图片文件。
- `ready video`:删除压缩后的 `.mp4` 和状态文件。
- `processing video`:写入删除标记或直接删除状态文件;后台任务结束后发现状态不存在即清理输出。
- `failed video`:删除状态文件和可能残留的临时文件。
列表接口不返回孤儿临时文件。启动时不要求恢复未完成任务;若服务重启导致任务中断,残留 `processing` 可以在下一次列表时标记为 `failed` 或由清理逻辑处理。
## 10. 测试策略
后端:
- 媒体 MIME/扩展名识别。
- 视频 id 与图片 id 的路径安全校验。
- 状态文件 `processing -> ready -> failed` 读写。
- 特殊文件夹权限:未注册 404、无权 403、有权通过。
- 删除会清理成品、状态和临时文件。
- ffmpeg 命令构造包含 720p、24fps、900k、96k、`+faststart`。
前端:
- 特殊文件夹图片上传和显示仍可用。
- 上传视频后立即出现“AI 正在压缩视频...”占位。
- 轮询到 `ready` 后替换为播放器。
- `failed` 状态显示失败卡并可删除。
- 视频拖拽、缩放、删除、caption、保存布局不破坏图片和装饰。
端到端:
- 在本地安装 `ffmpeg` 后上传一个小视频,确认转码完成、播放不卡、刷新后布局保留。
- 生产部署后用特殊文件夹验证上传视频、等待处理、播放、删除。
## 11. 部署与文档
这是后端改动,需要全量重部署:重编 Rust、上传二进制和前端静态文件、重启 Rust 服务。
服务器新增运行时依赖:
- `ffmpeg`
文档需要更新:
- `docs/photo-wall.md`:说明特殊文件夹支持视频媒体,个人墙暂不支持。
- `docs/superpowers/plans/2026-06-18-deploy.md`:补充安装 `ffmpeg` 和视频转码部署注意事项。
- `docs/superpowers/specs/2026-06-24-special-folders-design.md`:标注媒体接口和视频扩展已由本设计接续。