作品索引

Video Stream - Rust 视频流服务

RustAxumPostgreSQLMinIOFFmpegHLSPrometheus
Video Stream - Rust 视频流服务

视频服务真正难的部分,不是把一个文件放进对象存储,而是把“大文件上传、异步转码、私有播放和失败恢复”组织成一条可追踪的链路。video-stream 是我用 Rust 做的一套视频流后端,为移动端提供分片上传、断点续传、多码率 HLS 转码和短时效播放地址。

它把源片和播放产物分开保存:源文件是可重试的输入,HLS 清单和 fMP4 分片是播放器消费的输出。API 只负责控制面与清单重写,媒体分片最终从 MinIO 直连,避免大流量经过业务进程。

三个 crate,各自负责一条边界

项目是一个 Rust workspace,由三个 crate 组成:

  • vs-common:配置、统一错误 envelope、request id、数据库连接与迁移、PostgreSQL 仓储、MinIO S3 客户端和指标基础设施。
  • vs-api:Axum HTTP 服务,默认监听 8080,提供上传会话、视频管理、播放签发、HLS 清单和健康检查接口。
  • vs-worker:独立转码进程,轮询 PostgreSQL 任务队列,调用 ffprobe 与 FFmpeg,默认在 9464 暴露 worker 指标。

PostgreSQL 保存视频元数据、上传会话、转码任务和 rendition 索引;MinIO 保存源片、master.m3u8、各档位 playlist、fMP4 分片和封面;Prometheus 与 Grafana 负责把 API 和 worker 的状态放到同一张观测面上。

从分片上传到视频就绪

客户端不会把几百 MB 甚至数 GB 的文件转发给 API。创建上传会话后,API 返回 MinIO multipart upload 的预签名 URL,客户端直接并行上传分片。默认分片大小为 16 MiB,单文件上限为 4 GiB。

这条链路有几个刻意保留的检查点:

  1. POST /api/v1/uploads 写入 videosupload_sessions,并返回分片编号与短时效 URL。
  2. 网络中断后,presign 接口通过 MinIO ListParts 对账,只为缺失分片补签,客户端可以从断点继续。
  3. complete 在行锁下校验分片编号必须完整连续,调用 S3 合并后再次核对对象大小。
  4. 同一个数据库事务把视频置为 uploaded,同时插入 transcode_jobs;重复 complete 返回相同结果,不会重复入队。
  5. worker 使用 FOR UPDATE SKIP LOCKED 领取任务,租约默认为 10 分钟,并按三分之一租约周期续期。实例崩溃后,过期任务可以被另一个 worker 接管。

Video Stream 上传与转码流水线

worker 的流水线是“下载源片、探测、规划阶梯、转码、上传产物、事务落库”。ffprobe 会识别时长、分辨率和旋转信息;阶梯决策不会放大源片,横屏默认产出 1080p、720p、480p,竖屏按长边适配,小源只生成一个接近原尺寸的档位。

FFmpeg 输出 H.264/AAC 的 fMP4 HLS,分片目标时长为 6 秒,同时生成 master playlist 和封面。所有产物上传成功后,worker 在一个事务中整组替换 video_renditions、回填时长与宽高、把视频置为 ready,再标记任务成功。网络、存储和数据库问题按指数退避重试,源文件损坏等确定性错误直接进入 failed 终态。

播放链路把大流量留在对象存储

移动端先用用户中心的 access token 请求 GET /api/v1/videos/{id}/play。API 检查视频归属和 ready 状态后,签发绑定 video_iduser_id 的短时效播放 token,默认有效期 30 分钟。

播放器请求 HLS 清单时不能稳定地自定义 Authorization header,因此 master 和 variant 清单走 query token。API 不直接把 MinIO 的原始清单透传出去,而是:

  • 读取私有桶中的 master.m3u8,校验档位路径只能是 worker 生成的 {label}/playlist.m3u8
  • 为每个 variant 清单引用追加播放 token。
  • 读取 variant 清单,拒绝外部 URL、路径穿越、未知 URI 标签和异常文件名。
  • init.mp4.m4s 分片生成 MinIO 预签名地址,TTL 不超过播放 token 剩余时间。

这样 API 只处理轻量的清单和签名,真正的媒体分片从 MinIO 直连播放器;私有桶本身不需要开放匿名访问。

Video Stream 私有 HLS 播放链路

鉴权、隔离与限流

用户身份来自 business-auth。API 通过 authkit-rs 拉取并缓存 Ed25519 JWKS,在本地离线验签,不把每次请求都变成对用户中心的同步调用。业务接口只接受真实用户主体,user_id 永远取自 token,不接受客户端传入。

资源隔离采用“不暴露存在性”的策略:列表、详情、播放签发、删除和上传会话都按当前用户过滤,访问他人资源统一返回 404。播放 token 与用户和视频绑定,不能跨视频复用;播放清单对 URI 的严格白名单也避免了把 token 拼到外部地址上。

受保护接口同时使用用户桶和 IP 桶限流,播放清单只使用 IP 桶。默认配额为用户 120 次/分钟、突发 30,IP 600 次/分钟、突发 100;代理头只有在明确配置可信反向代理时才会被采信。错误统一返回带 request_id 的 envelope,便于客户端处理和日志追踪。

Video Stream 鉴权、限流与可观测性

工程验证

仓库提供 scripts/check.ps1,统一执行格式检查、Clippy 和 workspace 测试。测试覆盖上传会话、分片对账、完成幂等、任务租约、转码阶梯规划、HLS 清单重写、播放凭证、资源隔离、分层限流和健康检查;Prometheus 指标与 Grafana 预置看板用于观察 API 和 worker 的运行状态。

这个项目的重点不是“支持播放”四个字,而是把视频系统中最容易失控的边界显式化:上传与 API 解耦,转码与请求解耦,媒体与鉴权解耦,失败与重试解耦。最终形成的是一条从分片直传、异步转码到私有 HLS 播放的完整链路,并且每个阶段都有明确状态、恢复机制和观测指标。