视频上传不是把一个文件接收下来就结束了。平台还要校验文件、保存原始对象、提取媒体信息、转码、生成封面、更新业务状态,并在任何一步失败时留下可恢复的记录。
在 HAOVP 的内容链路中,我把业务记录、MinIO 对象存储和 FFmpeg 媒体处理串在一起。三者职责不同:数据库记录业务状态,MinIO 保存文件对象,FFmpeg 负责读取和转换媒体内容。
一条完整的上传链路
客户端选择文件
-> 创建上传任务
-> 校验文件信息
-> 上传到临时对象
-> 保存业务记录
-> FFmpeg 探测媒体
-> 转码与封面提取
-> 保存处理后对象
-> 更新视频状态
-> 前端查询处理结果这条链路既可以同步实现,也可以拆成异步任务。视频文件较大、转码耗时不可控时,异步处理更容易避免接口长时间占用连接。
先创建业务记录还是先上传文件
两种顺序都有需要处理的问题。
先创建业务记录:
- 可以立即获得任务 ID;
- 方便记录上传进度;
- 上传失败时会留下待清理记录。
先上传文件:
- 数据库不会提前产生业务记录;
- 文件上传后创建记录失败,会留下孤立对象。
我更关注每一步都能关联到同一个任务标识,并设计清理机制,而不是认为某种顺序天然不会产生垃圾数据。
文件校验不能只看扩展名
客户端传来的文件名不可信。后端至少需要检查:
- 文件是否为空;
- 大小是否超过限制;
- 扩展名是否允许;
- MIME 类型是否合理;
- 实际文件头是否符合格式;
- 文件名是否需要重新生成;
- 用户是否有上传权限;
- 当前配额是否允许继续上传。
扩展名和 MIME 类型都可能被伪造。真正进入媒体处理前,还需要让 FFmpeg 或探测工具确认文件能否解析。
MinIO 中怎样组织对象
对象存储更适合按 Bucket 和对象键管理文件,而不是让业务代码依赖服务器本地路径。
一个脱敏的对象键结构可以是:
videos/{date}/{taskId}/source.mp4
videos/{date}/{taskId}/output.mp4
covers/{date}/{taskId}/cover.jpg这是通用示例,不对应 HAOVP 的真实 Bucket、目录和地址。
对象键应由服务端生成,避免直接使用用户文件名。数据库中可以保存对象键和必要元数据,不应把存储凭据、内部端点和临时签名地址作为永久业务字段。
FFprobe 先读取媒体信息
在转码前,可以先使用 FFprobe 获取:
- 容器格式;
- 视频和音频编码;
- 时长;
- 分辨率;
- 帧率;
- 码率;
- 流数量。
ffprobe -v error -show_streams -show_format input.mp4命令是通用示例。实际执行时应通过受控参数调用,不把用户输入直接拼接成 Shell 命令。
探测失败时,任务应进入明确的失败状态,并记录脱敏后的错误摘要,而不是继续尝试播放一个无法识别的对象。
FFmpeg 转码的目标
转码不只是换扩展名。它可能涉及视频编码、音频编码、分辨率、码率和容器格式。
一个简化示例:
ffmpeg -i input.mp4 \
-c:v libx264 \
-c:a aac \
-movflags +faststart \
output.mp4+faststart 可以把 MP4 播放需要的元数据移动到文件前部,便于渐进播放。具体编码参数需要根据输入文件、资源和质量目标调整,不能直接把示例作为所有视频的固定方案。
封面怎样生成
封面可以由用户上传,也可以从视频中截取。
ffmpeg -ss 00:00:03 -i input.mp4 -frames:v 1 cover.jpg固定截取第几秒不一定合适:短视频可能没有该时间点,开头也可能是黑屏。后续可以根据视频时长选择位置,或者允许用户从候选帧中选择。
生成封面后,需要把图片保存到对象存储,并更新业务记录中的封面对象键。
处理状态要明确
前端不能只看到“上传成功”,因为文件进入 MinIO 后,转码可能仍在进行。
可以设置类似状态:
CREATED
UPLOADING
UPLOADED
PROCESSING
READY
FAILED具体名称可以调整,重点是区分上传和媒体处理。只有 READY 状态的视频才进入正常播放和公开展示。
失败状态还应该保存失败阶段,例如上传、探测、转码、封面或对象写入,方便判断能否重试。
异步任务与消息
上传接口完成后,可以提交异步处理任务:
保存上传完成状态
-> 提交视频处理事件
-> 消费者执行 FFmpeg
-> 上传输出文件
-> 更新最终状态消息成功发送不代表媒体处理成功。需要考虑:
- 消息重复消费;
- 消费者中途退出;
- 同一任务被并发处理;
- 转码成功但状态更新失败;
- 状态成功但对象上传失败;
- 重试是否覆盖已有对象。
任务处理要具备幂等性,并允许根据任务状态重新执行安全步骤。
临时文件与磁盘空间
FFmpeg 通常需要本地输入或输出文件。即使最终对象存储在 MinIO,处理节点仍可能使用临时目录。
需要关注:
- 临时文件名是否唯一;
- 任务完成或失败后是否清理;
- 磁盘空间是否足够;
- 多任务并发是否造成磁盘耗尽;
- 服务重启后怎样清理遗留文件;
- 文件权限是否限制其他进程访问。
不能只清理成功任务,异常路径更容易留下大文件。
删除和失败补偿
一个视频可能对应原始文件、转码文件、封面和数据库记录。删除时需要明确顺序和失败处理。
标记删除中
-> 禁止继续展示
-> 删除或安排清理对象
-> 处理互动与关联数据
-> 更新删除结果若对象删除失败,可以记录待清理任务,而不是让业务接口无限等待。反过来,也不能先删除数据库记录后失去对象键,导致存储中的文件难以追踪。
下载和播放地址
对象存储内部地址不应直接暴露给所有客户端。可以根据部署方式选择:
- 由网关或文件服务代理;
- 生成短期签名地址;
- 通过受控公开 Bucket;
- 配合 CDN 使用受限源站。
无论哪种方式,都不应在日志和文章中公开真实端点、Access Key、Secret Key 和永久签名链接。
如何验证链路
我会分别检查:
- 合法视频能够上传并进入处理;
- 非法格式和超大文件被拒绝;
- FFprobe 失败时状态正确;
- 转码和封面对象可读取;
- 失败任务可以安全重试;
- 重复消息不会产生多份业务记录;
- 临时文件能够清理;
- 未完成视频不会被正常展示;
- 删除后对象与业务状态一致。
视频上传、FFmpeg 转码和 MinIO 存储串起来以后,真正需要维护的是一条有状态、可重试、可清理的处理链路。数据库说明任务处于哪里,MinIO 保存对象,FFmpeg 完成媒体处理;每一步都要能够成功确认,也要能够在失败后找到恢复入口。