实现契约
这页记录跨模块正确性依赖的实现契约。修改 HTTP/gRPC handler、Provider 播放生成、播放 worker、读库路由、文件存储或 SQLx 查询前,先看这里。
传输层和 Impl 层
Section titled “传输层和 Impl 层”HTTP 和 gRPC 是传输适配层。它们负责解析 path、query、JSON/protobuf body、headers、状态码和 streaming body,然后调用 synctv-api/src/impls。
共享行为放在 impls 和 synctv-core:
- 权限和 actor 校验;
- 播放状态变更和资源观察;
- Provider 调用、缓存、URL 过期和生命周期 fanout;
- 聊天、文件、房间、媒体和直播业务规则;
- 上传/下载 stream 处理。
Protobuf request type 是共享参数契约。HTTP handler 可以先把 path 或 query 中的字段填入 request,再调用 impls。gRPC handler 直接把 protobuf 值传入同一条 impl 路径。传输层新增代码只负责解析、编码、metadata 提取和 stream 适配。
Provider 播放
Section titled “Provider 播放”MediaProvider::generate_playback 是 Provider 播放决策边界。每个 Provider 自己决定返回的所有 mode,包括 upstream/direct mode、proxy_* sibling、default_mode、headers、字幕、danmaku、thumbnail、manifest、过期时间、缓存 metadata 和直播资源生命周期数据。
Provider 签名在生成 playback 时完成。共享 helper 只做机械的 proxy_* sibling URL 生成;Provider 代码决定何时调用 helper、哪些 header 暴露给客户端、哪个 mode 作为默认值。
VersionedPlayback 是缓存 payload 和 proxy 查找索引。version 用于把 Provider 生成的 /stream、/m3u8、/mpd、字幕、danmaku、thumbnail、FLV 和 HLS segment URL 解析回同一个播放结果。Response finalization 只把当前请求的 signing key、room/user binding、version 和 expiry 交给 Provider 的 rewrite callback;mode、默认值、header 暴露、manifest metadata 和直播生命周期仍由该 Provider 决定。
App 客户端通常可以消费 provider 原生 URL,因此 upstream/direct mode 和 proxy sibling 同时可用时,Provider 应同时返回两类 mode。Provider 按 source 选择最稳妥的默认 mode:带 header 的 HLS 或 DASH 可以默认使用 proxy_*,简单直连文件可以默认使用 upstream URL。
Proxy 生成和 proxy 解析是同一个契约。Provider 只要生成 /stream、/m3u8、/mpd、字幕、danmaku、thumbnail、FLV、HLS playlist 或 HLS segment URL,就必须在同一个 Provider 中实现并测试对应 resolver。缓存命中和刷新生成的 playback 需要保持相同可用的 URL 形态。
Provider 返回内容的维护契约:
| Provider | Playback 内容契约 |
|---|---|
| Direct URL | 返回 upstream mode;可代理时同时返回 proxy_* mode;带 header 的源默认走 proxy;HLS proxy manifest 要重写 segment URL;Range 请求需要真实验证。 |
| Alist | 返回 upstream/direct 或转码 mode;同时返回 proxy_* sibling;默认 mode 由 Provider 按客户端能力选择;thumbnail、subtitle、HLS segment 和 stream proxy action 必须都有 resolver。 |
| Emby/Jellyfin | 返回 upstream/transcode mode 和 proxy sibling;upstream token header 按产品策略允许给客户端;proxy sibling 必须覆盖 direct stream、HLS/transcode 和字幕。 |
| Bilibili | 匿名可播放内容默认给出可用 direct/proxy mode;DASH/MPD 默认使用 proxy_* manifest;proxy manifest 的 segment URL 继续走 SyncTV;字幕、danmaku、thumbnail、缓存 metadata 和 CDN header 由 Bilibili Provider 自己维护。 |
| RTMP | Playback URL 指向 SyncTV 管理的 HLS/FLV provider-proxy action;publish key、stream info、HLS playlist/segment、FLV 和 idle cleanup 属于同一生命周期。 |
| live proxy | 外部 RTMP/HTTP-FLV 在首个 viewer 请求时拉流并注册为本节点 publisher;HLS/FLV URL 指向 SyncTV;idle cleanup 负责断开外部拉流并注销 publisher。 |
新增 mode 的规则:生成出来的每个 URL 都必须有对应 resolver;缓存命中、缓存失效、URL 过期、headers、manifest/segment/subtitle/danmaku/thumbnail 都要在同一个 Provider 内维护。
Provider 手动验证覆盖完整 PlaybackResult:
- Direct URL、Alist、Emby、Jellyfin、Bilibili 匿名播放、RTMP 和 live proxy;
- 返回给客户端的每个 playback mode 和每个 URL;
- direct URL、proxy URL、HLS/DASH manifest、indexed segment、字幕、danmaku、thumbnail、FLV、Range 请求、缓存命中/未命中、过期和清理;
- 用构建出的
synctv二进制和 CLI 搭环境,再用curl请求返回的 URL; - 动态 playlist 路径列表、媒体解析、播放生成、target 切换、封面/thumbnail 路由和自动切换后的 target 变化;
- RTMP 和 live proxy 完整生命周期:通过 CLI 创建,发布或拉取真实上游,请求 stream info、HLS playlist/segment、FLV,断开连接,然后验证 idle cleanup。
需要手动验证的工作流,先补 synctv CLI,再通过 CLI 加 curl 验证。
播放后台任务
Section titled “播放后台任务”播放后台任务使用当前进程的 active rooms 作为调度输入。Active 指本节点 ConnectionRuntime 中至少有一个 Realtime 连接。
这个契约用于时长探测、自动切换和播放资源生命周期任务。Worker 在每个节点运行。同一房间可以在多个节点 active,重复尝试由持久化层收敛:
- 时长探测 claim 使用数据库锁和
SKIP LOCKED; - 自动切换使用播放状态事务和乐观版本;
- 动态 playlist 任务连接
room_playback_progress.target_hash,让任务绑定当前选中的 target。
这些 worker 使用 ConnectionRuntime::active_room_ids()。Presence 和 hot-room 统计用于列表、管理视图、分析和 metrics。Leader election 用于分区维护、清理等全局单例任务。
时长探测查询需要同时保留 state.room_id = ANY($active_room_ids) 和 progress.target_hash = metadata.target_hash。前者保持本进程 active-room 调度边界,后者把动态 playlist 探测绑定到当前播放 target。
自动切换需要通过播放状态事务和 version 写入推进状态。多个节点同时扫描同一个 active room 时,最终只能有一次状态推进成功。
有限 sequential playlist 到达末尾时,自动切换需要通过同一条状态写入路径持久化稳定的结束状态。持久化后的结束状态让后续扫描周期跳过已经结束的 source。
读库按 repository method 显式选择。只有白名单内、允许最终一致的列表/发现类读取走 DatabasePools::read()。
这些操作使用主库:
- 写入、事务、migration 和 status 检查;
- 认证和授权检查;
- 会暴露过期凭据的 cache prewarm 和连接构建输入;
- 写入后的通知 fanout;
- cursor 来自主库的 snapshot-plus-cursor 读取;
- 播放 worker claim 和状态变更;
- 文件上传/下载 finalize 和 ownership 校验。
新增读库查询时,需要写明 replica lag 可接受的原因,并让 response cursor、cache 写入和副作用使用一致的数据来源。
文件存储有一个对象注册表,以及独立的字节表和会话表:
file_objects是对象注册表。validated_at表示对象已经可用,NULL表示 pending session placeholder。file_blob_parts保存 database 后端对象字节。Pending upload 先把 part 写到upload_session_key,finalize 时用UPDATE把这些行提升到最终object_key。file_upload_sessions保存 database 与 S3 的 single-object 或 multipart 上传状态。file_upload_session_parts保存已上传 part 进度,以及 provider ETag/checksum。
Database multipart 上传在写入每个 part 时校验该 part 的 SHA-256。Finalize 使用已保存的 part checksum 重新计算 manifest digest,然后用 UPDATE 把 file_blob_parts.object_key 从 upload_session_key 提升到最终 object_key。Pending bytes 和 final bytes 使用同一张 part 表,通过 object key 表达状态,resume/idempotency 校验开销较低。
Single-object 上传 session 仍使用服务端决定的 manifest plan,但通过一次 PUT 写入完整对象,并在 complete 时不写 part 进度行。S3 multipart session 返回客户端直传 S3 的 presigned part URL。服务端代理上传/下载路径作为需要部署兼容时的能力保留。Database 后端的 URL 指向 SyncTV,因为字节存储在应用背后的数据库里。
下载是流式的。HTTP 返回普通二进制响应,body 由服务端 stream 驱动;gRPC 返回 protobuf chunk stream。Core service 返回 FileObjectDownload,让两种传输共享同一个 storage 实现,主路径按 chunk 推进对象读取。
上传 part size 由服务端规划。修改规划影响新 session;已打开的 session 使用自己保存的 part_size_bytes 和 manifest。校验 part 时读取 session 保存的 plan,保证中途调整默认值时已有 session 继续按原 manifest resume。
SQLx Query Cache
Section titled “SQLx Query Cache”Repository 代码使用 SQLx checked macros。保留 checked macro 做查询形状校验,SQL 变化时同步更新 .sqlx:
cargo sqlx prepare --workspace -- --all-targetsSQLX_OFFLINE=true cargo check --workspace --all-targetsSQL 或查询返回列变化时,.sqlx metadata 属于同一个改动。
需要 compile-time SQL 校验的 repository 查询继续使用 checked macros。更新 .sqlx 是正确做法;为了绕开 cache 改成 unchecked query 会丢掉离线 CI 对查询参数、返回列和 nullability 的保护。
播放 worker SQL 变化还需要 active-room E2E 证据:没有本节点 WebSocket 时不探测 metadata,有本节点 WebSocket 时探测 metadata,动态 playlist 使用 target-hash 绑定,多个 worker 能看到同一房间时只提交一次自动切换。