248 lines
10 KiB
Markdown
248 lines
10 KiB
Markdown
# 设计:特殊文件夹视频媒体(上传后服务器压缩)
|
||
|
||
> 在特殊文件夹共享照片墙(`#<文件夹名>`)里支持视频作为墙上媒体元素。
|
||
> 视频上传后先显示处理中占位卡,服务器用 `ffmpeg` 压缩为低带宽 MP4,完成后在墙上播放。
|
||
> 本设计只开放给特殊文件夹;个人 `#photo` 暂不开放视频入口。
|
||
>
|
||
> 状态:规划中,当前代码尚未实现 `/media*` 接口、视频上传、转码和视频卡片渲染。
|
||
|
||
前置阅读:
|
||
- [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. 目标与范围
|
||
|
||
目标:
|
||
- 特殊文件夹支持图片和视频混合展示。
|
||
- 视频像照片一样可以上传、拖拽、缩放、置顶/置底、删除。
|
||
- 视频上传后不阻塞页面等待转码;墙上立即出现“thinking”风格的处理中卡片。
|
||
- 服务器将常见视频格式统一压缩为适合 3M 带宽播放的 MP4。
|
||
- 不提供显式视频下载入口,不添加下载按钮;这不是防下载安全边界。
|
||
|
||
非目标:
|
||
- 个人 `#photo` 视频上传。
|
||
- 多清晰度转码、自适应码率、HLS/DASH。
|
||
- 视频封面生成、时间轴截图、在线播放统计。
|
||
- 大视频断点续传。
|
||
|
||
## 2. 总体架构
|
||
|
||
前端把 `PhotoWallPage` 从“照片元素”扩展为“媒体元素”,新增 `video` 类型。特殊文件夹使用新的媒体 API 加载图片和视频;图片仍按现有视觉展示,视频按状态渲染为处理中卡片、失败卡片或 `<video controls preload="metadata">` 播放器。
|
||
|
||
后端把特殊文件夹存储从“图片列表”升级为“媒体列表”。图片可以同步保存并立即进入 `ready` 状态。视频上传后先写入临时文件,创建状态文件,后台调用 `ffmpeg` 转码;转码完成后只保留压缩后的 `.mp4` 和状态文件,删除原始临时文件。
|
||
|
||
`ffmpeg -movflags +faststart` 只用于优化浏览器在线播放体验:把 MP4 索引移动到文件开头,让 `<video>` 不必等完整文件下载完才开始播放。UI 不提供显式下载按钮;浏览器原生视频控件仍需加 `controlsList="nodownload"` 降低下载入口暴露,但这不构成防下载或防复制安全边界。
|
||
|
||
## 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='min(iw,if(gte(iw,ih),1280,720))':'min(ih,if(gte(iw,ih),720,1280))':force_original_aspect_ratio=decrease:force_divisible_by=2,fps=24" \
|
||
-c:v libx264 -preset veryfast -crf 30 -maxrate 900k -bufsize 1800k \
|
||
-c:a aac -b:a 96k \
|
||
-movflags +faststart output.mp4
|
||
```
|
||
|
||
实现计划必须用横屏、竖屏和近正方形视频测试该 filter,确认不会放大小视频,且输出不会超过
|
||
1280x720 或 720x1280 的目标边界。
|
||
|
||
并发策略:
|
||
- Rust 进程内视频转码并发限制为 1。
|
||
- 上传请求只负责接收文件、创建状态、启动后台任务,然后立即返回。
|
||
- 后台任务成功后把状态改为 `ready`,删除临时原视频。
|
||
- 后台任务失败后把状态改为 `failed`,删除临时原视频,保留短错误摘要。
|
||
|
||
## 7. 前端交互
|
||
|
||
特殊文件夹工具栏把“上传照片”改为“上传媒体”。选择文件、拖拽文件都支持图片和视频;剪贴板仍只要求保持图片粘贴可用,视频粘贴不是本期目标。
|
||
|
||
渲染规则:
|
||
- `image + ready`:沿用当前照片卡片。
|
||
- `video + processing`:显示软木板风格占位卡,文案为“thinking”。
|
||
- `video + ready`:显示带控件的 `<video controls preload="metadata">`,不自动播放。
|
||
- `video + failed`:显示“视频处理失败”,允许右键删除。
|
||
|
||
视频卡片行为:
|
||
- 支持拖拽、缩放、旋转、置顶/置底、删除。
|
||
- 双击编辑说明文字,逻辑与照片 caption 一致。
|
||
- 用户主动点击才播放,避免多个视频同时消耗 3M 带宽。
|
||
- 不显示显式下载按钮;`<video>` 使用 `controlsList="nodownload"`。该属性只是 UI 降噪,
|
||
不能当成安全控制。
|
||
|
||
轮询:
|
||
- 上传视频返回 `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` 一段时间以兼容旧前端,但新前端不再依赖它。
|
||
- 后端 state 接口必须同步迁移:`PUT /api/web/folder/{name}/state` 需要接受
|
||
`mediaLayout` 和旧 `photoLayout`;`GET` 返回 `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`。
|
||
- ffmpeg 缩放测试覆盖横屏、竖屏、正方形/近正方形和小尺寸输入,确认不超过目标尺寸且不放大。
|
||
|
||
前端:
|
||
- 特殊文件夹图片上传和显示仍可用。
|
||
- 上传视频后立即出现“thinking”占位。
|
||
- 轮询到 `ready` 后替换为播放器。
|
||
- `failed` 状态显示失败卡并可删除。
|
||
- 视频拖拽、缩放、删除、caption、保存布局不破坏图片和装饰。
|
||
- `<video>` 渲染包含 `controlsList="nodownload"`,且产品文案只承诺不提供下载入口,
|
||
不承诺防下载。
|
||
|
||
端到端:
|
||
- 在本地安装 `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`:标注媒体接口和视频扩展已由本设计接续。
|