[{"data":1,"prerenderedAt":310},["ShallowReactive",2],{"work-\u002Fworks\u002Fvideo-stream":3,"work-surround-\u002Fworks\u002Fvideo-stream":301},{"id":4,"title":5,"body":6,"cover":281,"date":282,"demo":283,"description":284,"draft":285,"extension":286,"featured":287,"meta":288,"navigation":287,"path":289,"repo":283,"seo":290,"stack":291,"stem":299,"updated":283,"__hash__":300},"works\u002Fworks\u002Fvideo-stream.md","Video Stream - Rust 视频流服务",{"type":7,"value":8,"toc":271},"minimark",[9,18,21,26,29,70,73,76,79,82,134,141,147,162,165,183,190,215,218,224,228,242,245,252,258,261,268],[10,11,12,13,17],"p",{},"视频服务真正难的部分，不是把一个文件放进对象存储，而是把“大文件上传、异步转码、私有播放和失败恢复”组织成一条可追踪的链路。",[14,15,16],"code",{},"video-stream"," 是我用 Rust 做的一套视频流后端，为移动端提供分片上传、断点续传、多码率 HLS 转码和短时效播放地址。",[10,19,20],{},"它把源片和播放产物分开保存：源文件是可重试的输入，HLS 清单和 fMP4 分片是播放器消费的输出。API 只负责控制面与清单重写，媒体分片最终从 MinIO 直连，避免大流量经过业务进程。",[22,23,25],"h2",{"id":24},"三个-crate各自负责一条边界","三个 crate，各自负责一条边界",[10,27,28],{},"项目是一个 Rust workspace，由三个 crate 组成：",[30,31,32,42,54],"ul",{},[33,34,35,41],"li",{},[36,37,38],"strong",{},[14,39,40],{},"vs-common","：配置、统一错误 envelope、request id、数据库连接与迁移、PostgreSQL 仓储、MinIO S3 客户端和指标基础设施。",[33,43,44,49,50,53],{},[36,45,46],{},[14,47,48],{},"vs-api","：Axum HTTP 服务，默认监听 ",[14,51,52],{},"8080","，提供上传会话、视频管理、播放签发、HLS 清单和健康检查接口。",[33,55,56,61,62,65,66,69],{},[36,57,58],{},[14,59,60],{},"vs-worker","：独立转码进程，轮询 PostgreSQL 任务队列，调用 ",[14,63,64],{},"ffprobe"," 与 FFmpeg，默认在 ",[14,67,68],{},"9464"," 暴露 worker 指标。",[10,71,72],{},"PostgreSQL 保存视频元数据、上传会话、转码任务和 rendition 索引；MinIO 保存源片、master.m3u8、各档位 playlist、fMP4 分片和封面；Prometheus 与 Grafana 负责把 API 和 worker 的状态放到同一张观测面上。",[22,74,75],{"id":75},"从分片上传到视频就绪",[10,77,78],{},"客户端不会把几百 MB 甚至数 GB 的文件转发给 API。创建上传会话后，API 返回 MinIO multipart upload 的预签名 URL，客户端直接并行上传分片。默认分片大小为 16 MiB，单文件上限为 4 GiB。",[10,80,81],{},"这条链路有几个刻意保留的检查点：",[83,84,85,99,110,116,127],"ol",{},[33,86,87,90,91,94,95,98],{},[14,88,89],{},"POST \u002Fapi\u002Fv1\u002Fuploads"," 写入 ",[14,92,93],{},"videos"," 和 ",[14,96,97],{},"upload_sessions","，并返回分片编号与短时效 URL。",[33,100,101,102,105,106,109],{},"网络中断后，",[14,103,104],{},"presign"," 接口通过 MinIO ",[14,107,108],{},"ListParts"," 对账，只为缺失分片补签，客户端可以从断点继续。",[33,111,112,115],{},[14,113,114],{},"complete"," 在行锁下校验分片编号必须完整连续，调用 S3 合并后再次核对对象大小。",[33,117,118,119,122,123,126],{},"同一个数据库事务把视频置为 ",[14,120,121],{},"uploaded","，同时插入 ",[14,124,125],{},"transcode_jobs","；重复 complete 返回相同结果，不会重复入队。",[33,128,129,130,133],{},"worker 使用 ",[14,131,132],{},"FOR UPDATE SKIP LOCKED"," 领取任务，租约默认为 10 分钟，并按三分之一租约周期续期。实例崩溃后，过期任务可以被另一个 worker 接管。",[10,135,136],{},[137,138],"img",{"alt":139,"src":140},"Video Stream 上传与转码流水线","\u002Fimages\u002Fworks\u002Fvideo-stream\u002Fupload-transcode-flow.svg",[10,142,143,144,146],{},"worker 的流水线是“下载源片、探测、规划阶梯、转码、上传产物、事务落库”。",[14,145,64],{}," 会识别时长、分辨率和旋转信息；阶梯决策不会放大源片，横屏默认产出 1080p、720p、480p，竖屏按长边适配，小源只生成一个接近原尺寸的档位。",[10,148,149,150,153,154,157,158,161],{},"FFmpeg 输出 H.264\u002FAAC 的 fMP4 HLS，分片目标时长为 6 秒，同时生成 master playlist 和封面。所有产物上传成功后，worker 在一个事务中整组替换 ",[14,151,152],{},"video_renditions","、回填时长与宽高、把视频置为 ",[14,155,156],{},"ready","，再标记任务成功。网络、存储和数据库问题按指数退避重试，源文件损坏等确定性错误直接进入 ",[14,159,160],{},"failed"," 终态。",[22,163,164],{"id":164},"播放链路把大流量留在对象存储",[10,166,167,168,171,172,174,175,178,179,182],{},"移动端先用用户中心的 access token 请求 ",[14,169,170],{},"GET \u002Fapi\u002Fv1\u002Fvideos\u002F{id}\u002Fplay","。API 检查视频归属和 ",[14,173,156],{}," 状态后，签发绑定 ",[14,176,177],{},"video_id"," 与 ",[14,180,181],{},"user_id"," 的短时效播放 token，默认有效期 30 分钟。",[10,184,185,186,189],{},"播放器请求 HLS 清单时不能稳定地自定义 ",[14,187,188],{},"Authorization"," header，因此 master 和 variant 清单走 query token。API 不直接把 MinIO 的原始清单透传出去，而是：",[30,191,192,199,202,205],{},[33,193,194,195,198],{},"读取私有桶中的 master.m3u8，校验档位路径只能是 worker 生成的 ",[14,196,197],{},"{label}\u002Fplaylist.m3u8","。",[33,200,201],{},"为每个 variant 清单引用追加播放 token。",[33,203,204],{},"读取 variant 清单，拒绝外部 URL、路径穿越、未知 URI 标签和异常文件名。",[33,206,207,208,178,211,214],{},"为 ",[14,209,210],{},"init.mp4",[14,212,213],{},".m4s"," 分片生成 MinIO 预签名地址，TTL 不超过播放 token 剩余时间。",[10,216,217],{},"这样 API 只处理轻量的清单和签名，真正的媒体分片从 MinIO 直连播放器；私有桶本身不需要开放匿名访问。",[10,219,220],{},[137,221],{"alt":222,"src":223},"Video Stream 私有 HLS 播放链路","\u002Fimages\u002Fworks\u002Fvideo-stream\u002Fplayback-flow.svg",[22,225,227],{"id":226},"鉴权隔离与限流","鉴权、隔离与限流",[10,229,230,231,234,235,238,239,241],{},"用户身份来自 ",[14,232,233],{},"business-auth","。API 通过 ",[14,236,237],{},"authkit-rs"," 拉取并缓存 Ed25519 JWKS，在本地离线验签，不把每次请求都变成对用户中心的同步调用。业务接口只接受真实用户主体，",[14,240,181],{}," 永远取自 token，不接受客户端传入。",[10,243,244],{},"资源隔离采用“不暴露存在性”的策略：列表、详情、播放签发、删除和上传会话都按当前用户过滤，访问他人资源统一返回 404。播放 token 与用户和视频绑定，不能跨视频复用；播放清单对 URI 的严格白名单也避免了把 token 拼到外部地址上。",[10,246,247,248,251],{},"受保护接口同时使用用户桶和 IP 桶限流，播放清单只使用 IP 桶。默认配额为用户 120 次\u002F分钟、突发 30，IP 600 次\u002F分钟、突发 100；代理头只有在明确配置可信反向代理时才会被采信。错误统一返回带 ",[14,249,250],{},"request_id"," 的 envelope，便于客户端处理和日志追踪。",[10,253,254],{},[137,255],{"alt":256,"src":257},"Video Stream 鉴权、限流与可观测性","\u002Fimages\u002Fworks\u002Fvideo-stream\u002Fsecurity-observability.svg",[22,259,260],{"id":260},"工程验证",[10,262,263,264,267],{},"仓库提供 ",[14,265,266],{},"scripts\u002Fcheck.ps1","，统一执行格式检查、Clippy 和 workspace 测试。测试覆盖上传会话、分片对账、完成幂等、任务租约、转码阶梯规划、HLS 清单重写、播放凭证、资源隔离、分层限流和健康检查；Prometheus 指标与 Grafana 预置看板用于观察 API 和 worker 的运行状态。",[10,269,270],{},"这个项目的重点不是“支持播放”四个字，而是把视频系统中最容易失控的边界显式化：上传与 API 解耦，转码与请求解耦，媒体与鉴权解耦，失败与重试解耦。最终形成的是一条从分片直传、异步转码到私有 HLS 播放的完整链路，并且每个阶段都有明确状态、恢复机制和观测指标。",{"title":272,"searchDepth":273,"depth":273,"links":274},"",3,[275,277,278,279,280],{"id":24,"depth":276,"text":25},2,{"id":75,"depth":276,"text":75},{"id":164,"depth":276,"text":164},{"id":226,"depth":276,"text":227},{"id":260,"depth":276,"text":260},"\u002Fimages\u002Fworks\u002Fvideo-stream\u002Fproject-cover.svg","2026-07-11",null,"基于 Rust、MinIO 与 FFmpeg 的视频上传、转码和私有 HLS 播放服务，支持分片直传、断点续传、任务接管和短时效播放凭证。",false,"md",true,{},"\u002Fworks\u002Fvideo-stream",{"title":5,"description":284},[292,293,294,295,296,297,298],"Rust","Axum","PostgreSQL","MinIO","FFmpeg","HLS","Prometheus","works\u002Fvideo-stream","NNCOHi0YAVmhS_Z3jSRU_riPus-VbAebft4-IItRKWA",[302,306],{"title":303,"path":304,"stem":305,"children":-1},"Moonstream 南月之星河","\u002Fworks\u002Fmoonstream","works\u002Fmoonstream",{"title":307,"path":308,"stem":309,"children":-1},"海燕边缘网关 haiyan-io","\u002Fworks\u002Fhaiyan-io-work","works\u002Fhaiyan-io-work",1787046421621]