10 KiB
设计:特殊文件夹视频媒体(上传后服务器压缩)
在特殊文件夹共享照片墙(
#<文件夹名>)里支持视频作为墙上媒体元素。 视频上传后先显示处理中占位卡,服务器用ffmpeg压缩为低带宽 MP4,完成后在墙上播放。 本设计只开放给特殊文件夹;个人#photo暂不开放视频入口。状态:规划中,当前代码尚未实现
/media*接口、视频上传、转码和视频卡片渲染。
前置阅读:
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. 媒体模型
特殊文件夹媒体项:
type MediaType = 'image' | 'video'
type MediaStatus = 'ready' | 'processing' | 'failed'
interface FolderMediaItem {
id: string
type: MediaType
status: MediaStatus
}
服务器为视频维护状态文件,建议路径为 SPECIAL_DIR/<folder>/media/<id>.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 + 白名单 | 删除媒体文件、临时文件和状态文件 |
上传响应:
{
"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 参数方向:
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:
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:标注媒体接口和视频扩展已由本设计接续。